diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3ea1498..1e0da34c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,5 @@ +name: CI + on: pull_request: workflow_dispatch: @@ -8,71 +10,44 @@ concurrency: cancel-in-progress: true # Do not add permissions here! Configure them at the job level! -permissions: {} +permissions: + contents: read jobs: - build-and-test-native: + build-and-test: + name: test (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-arm, macos-15] - steps: - - uses: actions/checkout@v4 + os: + - macos-15 # Apple Silicon + - macos-15-intel # Intel Mac + - ubuntu-22.04 # Linux x86_64 + - ubuntu-24.04-arm # Linux ARM64 + defaults: + run: + shell: bash - - name: Does init() in platform/src/lib.rs contain all roc_fx functions? (Imperfect check) - run: cat platform/src/lib.rs | grep -oP 'roc_fx_[^(\s]*' | sort | uniq -u | grep -q . && exit 1 || exit 0 + steps: + - name: Checkout + uses: actions/checkout@v4 - - uses: roc-lang/setup-roc@39c354a6a838a0089eea9068a0414f49b62c5c08 + - name: Install Zig + uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # ratchet:mlugg/setup-zig@v2.0.5 with: - # Note: nightly hashes are not verified because they are updated regularly. - version: nightly + version: 0.16.0 - - run: roc version - - - name: Install dependencies (Ubuntu) + - name: Install expect (Ubuntu) if: startsWith(matrix.os, 'ubuntu-') - run: | - sudo apt install -y expect ncat ripgrep + run: sudo apt-get install -y expect - - name: Install dependencies (macOS) + - name: Install expect (macOS) if: startsWith(matrix.os, 'macos-') - run: | - brew install expect # expect for testing - brew install nmap # includes ncat, for tcp-client example - brew install ripgrep # ripgrep for ci/check_all_exposed_funs_tested.roc - - - run: expect -v + run: brew install expect - name: Run all tests - run: ROC=roc EXAMPLES_DIR=./examples/ ./ci/all_tests.sh - - - name: Install dependencies for musl build - if: startsWith(matrix.os, 'ubuntu-') - run: | - sudo apt-get install -y musl-tools - if [[ "${{ matrix.os }}" == *"-arm" ]]; then - # TODO re-enable once TODO below is done: rustup target add aarch64-unknown-linux-musl - echo "no-op" - else - rustup target add x86_64-unknown-linux-musl - fi - - - name: Test building with musl target - if: startsWith(matrix.os, 'ubuntu-') - env: - ROC: roc - run: | - if [[ "${{ matrix.os}}" == *"-arm" ]]; then - # TODO debug this: CARGO_BUILD_TARGET=aarch64-unknown-linux-musl $ROC build.roc - echo "no-op" - else - CARGO_BUILD_TARGET=x86_64-unknown-linux-musl $ROC build.roc - fi - - - name: Test using musl build - if: startsWith(matrix.os, 'ubuntu-') - run: | - # TODO remove `if` when above TODOs are done - if [[ "${{ matrix.os }}" != *"-arm" ]]; then - NO_BUILD=1 IS_MUSL=1 ROC=roc EXAMPLES_DIR=./examples/ ./ci/all_tests.sh - fi + uses: roc-lang/roc/.github/actions/flaky-retry@main + with: + command: ./ci/all_tests.sh + error_string_contains: "error: (unable|invalid HTTP response)|HttpConnectionClosing" diff --git a/.github/workflows/ci_nix.yml b/.github/workflows/ci_nix.yml deleted file mode 100644 index 6c1561c6..00000000 --- a/.github/workflows/ci_nix.yml +++ /dev/null @@ -1,54 +0,0 @@ -on: - pull_request: - workflow_dispatch: - -# this cancels workflows currently in progress if you start a new one -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# Do not add permissions here! Configure them at the job level! -permissions: {} - -jobs: - build-and-test-nix: - strategy: - fail-fast: false - matrix: - # macos-15-intel uses x86-64 machine, macos-14 & 15 use aarch64 - os: [macos-15-intel, macos-15, ubuntu-22.04, ubuntu-24.04-arm] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - # install nix - - uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # commit for v31 - with: - nix_path: nixpkgs=channel:nixos-unstable - - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # commit for v16 - with: - name: enigmaticsunrise - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - - name: print architecture - run: uname -m - - - name: Run all tests - run: nix develop -c sh -c 'export ROC=roc && export EXAMPLES_DIR=./examples/ && ./ci/all_tests.sh' - - - name: Run all tests with debug compiler - env: - RUST_BACKTRACE: 1 - run: | - # use debug compiler - sed -i.bak 's/rocPkgs\.cli/rocPkgs.cli-debug/g' flake.nix - # make sure substitution was made - grep -q "rocPkgs.cli-debug" flake.nix - # docs does not work with debug compiler - sed -i.bak 's/\$ROC docs.*//g' ./ci/all_tests.sh - # for SINGLE_TAG_GLUE_CHECK_OFF=1 see github.com/roc-lang/basic-cli/issues/242 - nix develop -c sh -c 'export SINGLE_TAG_GLUE_CHECK_OFF=1 && export ROC=roc && export EXAMPLES_DIR=./examples/ && ./ci/all_tests.sh' - - - name: Check if jump-start script still works - run: nix develop -c sh -c 'export ROC=roc && bash ./jump-start.sh' diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml deleted file mode 100644 index c72bec1b..00000000 --- a/.github/workflows/deploy-docs.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Deploy docs to Pages - -on: - push: - branches: - - main - paths: - - '**.roc' - release: - types: - - created - - workflow_dispatch: - -# this cancels workflows currently in progress if you start a new one -concurrency: - group: "pages" - cancel-in-progress: true - -# Do not add permissions here! Configure them at the job level! -permissions: - contents: read - -jobs: - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-24.04 - permissions: - pages: write - id-token: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Pages - uses: actions/configure-pages@v5 - - - uses: roc-lang/setup-roc@39c354a6a838a0089eea9068a0414f49b62c5c08 - with: - # Note: nightly hashes are not verified because they are updated regularly. - version: nightly - - - run: roc version - - - name: Create temp directory for docs - run: mkdir -p ./temp_docs - - - name: Download and extract docs for each release - run: | - # Get all releases - releases=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/roc-lang/basic-cli/releases" | jq -c '.') - echo "$releases" | jq -c '.[]' | while read -r release; do - release_name=$(echo $release | jq -r '.tag_name') - assets_url=$(echo $release | jq -r '.assets_url') - - # Get assets for this release - assets=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "${assets_url}") - - # Look for docs.tar.gz asset - download_url=$(echo $assets | jq -r '.[] | select(.name=="docs.tar.gz") | .browser_download_url') - - if [ ! -z "$download_url" ]; then - echo "Processing release ${release_name}, downloading from ${download_url}" - - # Create directory for this release - mkdir -p "./temp_docs/${release_name}" - - # Download and extract - curl -sL "${download_url}" -o ./temp_docs/temp.tar.gz - tar -xzf ./temp_docs/temp.tar.gz -C "./temp_docs/${release_name}" --strip-components=1 - rm ./temp_docs/temp.tar.gz - else - echo "Error: docs.tar.gz not found for release ${release_name}" - fi - done - - # fix URLs - find ./temp_docs -type f -exec sed -i 's/\/packages\/basic-cli\//\/basic-cli\//g' {} + - - # Get the latest release version - latest_release=$(echo "${releases}" | jq -r '.[0].tag_name') - - if [ -f "./docs/index.html" ]; then - # Copy the index.html and replace LATESTVERSION with actual latest release - cat ./docs/index.html | sed "s/LATESTVERSION/${latest_release}/g" > ./temp_docs/index.html - echo "Created index.html with latest version: ${latest_release}" - else - echo "Error: index.html not found in docs folder" - exit 1 - fi - - - name: Add docs for main branch - env: - ROC_DOCS_URL_ROOT: /basic-cli/main - run: | - roc docs ./platform/main.roc - - mkdir -p "./temp_docs/main" - - mv ./generated-docs/* ./temp_docs/main - - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload the processed docs folder - path: "./temp_docs" - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..a326b768 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,204 @@ +name: Release + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag (e.g., v0.1.0)' + required: true + type: string + pull_request: + +# Ensure only one release workflow runs at a time +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Limit permissions of the GITHUB_TOKEN (override at job level as needed) +permissions: + contents: read + +jobs: + build-and-bundle: + name: Build and Bundle Platform + runs-on: macos-15 # macOS can cross-compile to all targets + outputs: + bundle_filename: ${{ steps.bundle.outputs.bundle_filename }} + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Zig + uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # ratchet:mlugg/setup-zig@v2.0.5 + with: + version: 0.16.0 + + - name: Install Rust with cross-compilation targets + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-apple-darwin,aarch64-apple-darwin,x86_64-unknown-linux-musl,aarch64-unknown-linux-musl + + - name: Build Roc from pinned commit + run: ./ci/build_pinned_roc.sh + + - name: Build platform for all targets + run: ./build.sh --all + + - name: Bundle platform + id: bundle + run: | + # Run bundle.sh and capture output + BUNDLE_OUTPUT=$(./bundle.sh 2>&1) + echo "$BUNDLE_OUTPUT" + + # Extract the tar.zst filename from "Created: /path/to/HASH.tar.zst" line + BUNDLE_PATH=$(echo "$BUNDLE_OUTPUT" | grep "^Created:" | awk '{print $2}') + BUNDLE_FILENAME=$(basename "$BUNDLE_PATH") + + if [ -z "$BUNDLE_FILENAME" ]; then + echo "Error: Could not extract bundle filename from output" + echo "Bundle output was:" + echo "$BUNDLE_OUTPUT" + exit 1 + fi + + echo "Bundle created: $BUNDLE_FILENAME" + echo "bundle_filename=$BUNDLE_FILENAME" >> "$GITHUB_OUTPUT" + + - name: Upload bundle artifact + uses: actions/upload-artifact@v4 + with: + name: platform-bundle + path: ${{ steps.bundle.outputs.bundle_filename }} + retention-days: 30 + + test-bundle: + name: Test Bundle (${{ matrix.os }}) + needs: build-and-bundle + strategy: + fail-fast: false + matrix: + os: + - macos-15 # Apple Silicon + - macos-15-intel # Intel Mac + - ubuntu-22.04 # Linux x86_64 + - ubuntu-24.04-arm # Linux ARM64 + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Zig + uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # ratchet:mlugg/setup-zig@v2.0.5 + with: + version: 0.16.0 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install expect (Ubuntu) + if: startsWith(matrix.os, 'ubuntu-') + run: sudo apt-get install -y expect + + - name: Install expect (macOS) + if: startsWith(matrix.os, 'macos-') + run: brew install expect + + - name: Build Roc from pinned commit + run: ./ci/build_pinned_roc.sh + + - name: Download bundle artifact + uses: actions/download-artifact@v4 + with: + name: platform-bundle + + - name: Start HTTP server for bundle + run: | + # Start Python HTTP server in background on port 8000 + python3 -m http.server 8000 & + HTTP_SERVER_PID=$! + echo "HTTP_SERVER_PID=$HTTP_SERVER_PID" >> "$GITHUB_ENV" + + # Wait for server to start + sleep 5 + + # Verify server is running and bundle is accessible + if ! curl -f -I "http://localhost:8000/${{ needs.build-and-bundle.outputs.bundle_filename }}" > /dev/null 2>&1; then + echo "Error: HTTP server not accessible or bundle not found" + kill $HTTP_SERVER_PID || true + exit 1 + fi + + echo "HTTP server started on port 8000 (PID: $HTTP_SERVER_PID)" + echo "Bundle accessible at: http://localhost:8000/${{ needs.build-and-bundle.outputs.bundle_filename }}" + + - name: Modify examples to use bundled platform + run: | + # Replace local platform path with HTTP URL in all examples + for example in examples/*.roc; do + sed -i.bak "s|platform \"../platform/main.roc\"|platform \"http://localhost:8000/${{ needs.build-and-bundle.outputs.bundle_filename }}\"|g" "$example" + done + + # Show diff for verification + echo "Modified examples:" + git diff examples/ | head -50 || true + + - name: Run tests with bundled platform + uses: roc-lang/roc/.github/actions/flaky-retry@main + with: + command: bash ci/all_tests.sh + error_string_contains: "error: (unable|invalid HTTP response)|HttpConnectionClosing" + env: + # Skip native build and rebundling since examples already point at the + # downloaded bundle served by the previous step. + NO_BUILD: "1" + NO_BUNDLE: "1" + + create-release: + name: Create GitHub Release + needs: [build-and-bundle, test-bundle] + runs-on: ubuntu-24.04 + # Only run on manual trigger + if: github.event_name == 'workflow_dispatch' + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download bundle artifact + uses: actions/download-artifact@v4 + with: + name: platform-bundle + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + BUNDLE_FILE=$(ls *.tar.zst | head -1) + + if [ -z "$BUNDLE_FILE" ]; then + echo "Error: No bundle file found" + exit 1 + fi + + ROC_COMMIT=$(python3 ci/get_roc_commit.py | cut -c1-8) + + # Create release with auto-generated notes + gh release create "${{ github.event.inputs.release_tag }}" \ + "$BUNDLE_FILE" \ + --title "${{ github.event.inputs.release_tag }}" \ + --generate-notes \ + --notes "Platform bundle built with Rust (stable) and Roc $ROC_COMMIT" + + echo "Release created: ${{ github.event.inputs.release_tag }}" + echo "Bundle uploaded: $BUNDLE_FILE" diff --git a/.github/workflows/test_latest_release.yml b/.github/workflows/test_latest_release.yml deleted file mode 100644 index 9fcb8e72..00000000 --- a/.github/workflows/test_latest_release.yml +++ /dev/null @@ -1,46 +0,0 @@ -on: - workflow_dispatch: - -# this cancels workflows currently in progress if you start a new one -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# Do not add permissions here! Configure them at the job level! -permissions: {} - -jobs: - test-latest-release: - runs-on: [ubuntu-22.04] - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - - name: install dependencies - run: sudo apt install -y expect ncat ripgrep - - - name: remove everything except some ci scripts - run: | - mkdir temp - mv ./ci/test_latest_release.sh temp - mv ./ci/get_latest_release_git_files.sh temp - mv ./ci/rust_http_server temp - find . -mindepth 1 -maxdepth 1 ! -name 'temp' -exec rm -rf {} + - - - name: Get all git files of the latest basic-cli release - run: ./temp/get_latest_release_git_files.sh - - - name: Use ./ci/test_latest_release.sh of the latest git main - run: mv -f ./temp/test_latest_release.sh ./ci/ - - - name: Remove things that dont work on musl - run: | - rm ./examples/file-accessed-modified-created-time.roc - sed -i.bak -e '/time_accessed!,$/d' -e '/time_modified!,$/d' -e '/time_created!,$/d' -e '/^time_accessed!/,/^$/d' -e '/^time_modified!/,/^$/d' -e '/^time_created!/,/^$/d' -e '/^import Utc exposing \[Utc\]$/d' ./platform/File.roc - rm ./platform/File.roc.bak - - - name: Run all tests with latest roc release + latest basic-cli release - run: EXAMPLES_DIR=./examples/ ./ci/test_latest_release.sh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index dcffab4a..567ca2c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ +### Do not modify these first three ignore rules. Needed to ignore files with no extension ### +# Ignore all files including binary files that have no extension +* +# Unignore all files with extensions +!*.* +# Unignore all directories +!*/ + + *.rh* *.rm* dynhost @@ -28,6 +37,7 @@ vgcore.* #editors .idea/ .vscode/ +.claude/ .ignore .exrc .vimrc @@ -47,17 +57,14 @@ bench-folder* *.roc-format-failed-ast-after *.roc-format-failed-ast-before -# Tests -file-test -example-stdin -http-get-json -file-read-buffered -temp-dir -env-var +# Test artifacts out.txt -# local roc folder for testing -roc_nightly +# Downloaded Roc nightly +roc_nightly* + +# Roc source built by CI +roc-src # see examples/dir.roc dirExampleE @@ -67,13 +74,17 @@ dirExampleD # see ci/all_tests.sh all_tests_output.log +# Old build artifacts (platform/*.a was pre-migration location) platform/*.a -ci/check_all_exposed_funs_tested -ci/check_cargo_versions_match - -# glue generated files -crates/roc_std - # build script artifacts build + +# Platform host libraries (we manually add things to track like crt1.o, libc.a, libunwind.a) +platform/targets/*/libhost.a + +# Cargo lock for reproducible builds +!Cargo.lock + +# Bundled platform package +*.tar.zst diff --git a/.roc-version b/.roc-version new file mode 100644 index 00000000..8a6c2ca3 --- /dev/null +++ b/.roc-version @@ -0,0 +1 @@ +3a5766840064f5e7c6080548a05607612acd6c98 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23d78af0..a2d29ca5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,14 +4,38 @@ We are committed to providing a friendly, safe and welcoming environment for all. See the [Code of Conduct](https://github.com/roc-lang/roc/blob/main/CODE_OF_CONDUCT.md) for details. -## How to generate docs? +## How to update the Roc version -You can generate the documentation locally and then start a web server to host your files. +The pinned Roc compiler commit lives in `.roc-version`. To update it: + +1. Update `.roc-version` to the full 40-character Roc commit SHA. +2. Run `./ci/regenerate_glue.sh` to refresh `src/roc_platform_abi.rs`. +3. Run `cargo check` and `./ci/all_tests.sh` to verify the new compiler works. + +## Documentation + +Generated Roc docs are currently disabled for this migration branch because `roc docs` is not implemented in the new compiler backend yet. Update this section and restore docs CI when that command is available again. + +## Regenerating Rust Glue + +When the platform API changes in `platform/*.roc`, regenerate the Rust ABI bindings instead of editing them by hand: + +```bash +./ci/regenerate_glue.sh +``` + +The script writes `src/roc_platform_abi.rs` using Roc's `RustGlue.roc` generator. It defaults to a sibling `../roc` checkout, and you can override paths when needed: ```bash -roc docs platform/main.roc -cd generated-docs -simple-http-server --nocache --index # comes pre-installed if you use `nix develop`, otherwise use `cargo install simple-http-server`. +ROC=../roc/zig-out/bin/roc ROC_SRC=../roc ./ci/regenerate_glue.sh ``` -Open http://0.0.0.0:8000 in your browser +To verify the checked-in glue is current without modifying the worktree: + +```bash +./ci/regenerate_glue.sh --check +``` + +Commit the platform API change, the regenerated `src/roc_platform_abi.rs`, and any required Rust host implementation updates together. Do not edit the generated glue file manually. + +Run `./ci/all_tests.sh` before opening a release or CI-facing PR. diff --git a/Cargo.lock b/Cargo.lock index d5cdcf99..f891dba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "addr2line" @@ -17,12 +17,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "backtrace" version = "0.3.75" @@ -40,15 +34,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bytes" -version = "1.11.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" @@ -155,7 +149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -181,15 +175,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" @@ -211,19 +205,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasi", ] [[package]] @@ -232,15 +214,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "host" -version = "0.0.1" -dependencies = [ - "roc_env", - "roc_host", - "roc_std", -] - [[package]] name = "http" version = "1.3.1" @@ -345,9 +318,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libsqlite3-sys" @@ -393,15 +366,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" -dependencies = [ - "libc", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -419,7 +383,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.59.0", ] @@ -503,12 +467,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "redox_syscall" version = "0.5.18" @@ -526,145 +484,29 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom", "libc", "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "roc_command" -version = "0.0.1" -dependencies = [ - "roc_io_error", - "roc_std", -] - -[[package]] -name = "roc_env" -version = "0.0.1" -dependencies = [ - "roc_file", - "roc_std", - "sys-locale", -] - -[[package]] -name = "roc_file" -version = "0.0.1" -dependencies = [ - "memchr", - "roc_io_error", - "roc_std", - "roc_std_heap", -] - [[package]] name = "roc_host" version = "0.0.1" dependencies = [ - "backtrace", "bytes", "crossterm", + "getrandom", "http-body-util", "hyper", "hyper-rustls", "hyper-util", "libc", - "memchr", - "memmap2", - "roc_command", - "roc_env", - "roc_file", - "roc_http", - "roc_io_error", - "roc_random", - "roc_sqlite", - "roc_std", - "roc_std_heap", - "roc_stdio", + "libsqlite3-sys", "sys-locale", "tokio", ] -[[package]] -name = "roc_host_bin" -version = "0.0.1" -dependencies = [ - "roc_env", - "roc_host", - "roc_std", -] - -[[package]] -name = "roc_http" -version = "0.0.1" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-rustls", - "memchr", - "roc_file", - "roc_io_error", - "roc_std", - "roc_std_heap", - "tokio", -] - -[[package]] -name = "roc_io_error" -version = "0.0.1" -dependencies = [ - "roc_std", - "roc_std_heap", -] - -[[package]] -name = "roc_random" -version = "0.0.1" -dependencies = [ - "getrandom 0.3.3", - "roc_io_error", - "roc_std", -] - -[[package]] -name = "roc_sqlite" -version = "0.0.1" -dependencies = [ - "libsqlite3-sys", - "roc_std", - "roc_std_heap", - "thread_local", -] - -[[package]] -name = "roc_std" -version = "0.0.1" -source = "git+https://github.com/roc-lang/roc.git#caaae472ee1e2d3ecca29168ca9292ed268e302a" -dependencies = [ - "arrayvec", - "static_assertions", -] - -[[package]] -name = "roc_std_heap" -version = "0.0.1" -source = "git+https://github.com/roc-lang/roc.git#caaae472ee1e2d3ecca29168ca9292ed268e302a" -dependencies = [ - "memmap2", - "roc_std", -] - -[[package]] -name = "roc_stdio" -version = "0.0.1" -dependencies = [ - "roc_io_error", - "roc_std", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -681,7 +523,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -721,9 +563,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -820,12 +662,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "subtle" version = "2.6.1" @@ -852,16 +688,6 @@ dependencies = [ "libc", ] -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "tokio" version = "1.45.0" @@ -956,24 +782,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1093,12 +901,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - [[package]] name = "zeroize" version = "1.8.2" diff --git a/Cargo.toml b/Cargo.toml index bf6510e0..5fc00030 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,66 +1,35 @@ -[workspace] -resolver = "2" -members = [ - "crates/roc_command", - "crates/roc_host", - "crates/roc_host_lib", - "crates/roc_file", - "crates/roc_host_bin", - "crates/roc_http", - "crates/roc_io_error", - "crates/roc_stdio", - "crates/roc_env", - "crates/roc_sqlite", - "crates/roc_random", -] - -[workspace.package] +[package] +name = "roc_host" +version = "0.0.1" authors = ["The Roc Contributors"] edition = "2021" license = "UPL-1.0" repository = "https://github.com/roc-lang/basic-cli" -version = "0.0.1" -[profile.release] -lto = true -strip = "debuginfo" -# You can comment this out if you hit a segmentation fault similar to the one in see issue github.com/roc-lang/roc/issues/6121 -# Setting this to 1 should improve execution speed by making things easier to optimize for LLVM. -# codegen-units = 1 +[lib] +name = "host" +crate-type = ["staticlib"] -[workspace.dependencies] -roc_std = { git = "https://github.com/roc-lang/roc.git" } -roc_std_heap = { git = "https://github.com/roc-lang/roc.git" } -roc_command = { path = "crates/roc_command" } -roc_file = { path = "crates/roc_file" } -roc_host = { path = "crates/roc_host" } -roc_http = { path = "crates/roc_http" } -roc_io_error = { path = "crates/roc_io_error" } -roc_stdio = { path = "crates/roc_stdio" } -roc_env = { path = "crates/roc_env" } -roc_random = { path = "crates/roc_random" } -roc_sqlite = { path = "crates/roc_sqlite" } -memchr = "=2.7.4" -hyper = { version = "=1.6.0", default-features = false, features = [ - "http1", - "client", -] } -hyper-util = "=0.1.12" -hyper-rustls = { version = "=0.27.6", default-features = false, features = [ - "http1", - "tls12", - "native-tokio", - "rustls-native-certs", # required for with_native_roots - "ring", # required for with_native_roots -] } -http-body-util = "=0.1.3" -tokio = { version = "=1.45.0", default-features = false } -sys-locale = "=0.3.2" -bytes = "=1.11.1" +[dependencies] crossterm = "=0.29.0" -memmap2 = "=0.9.4" -libc = "=0.2.172" -backtrace = "=0.3.75" +getrandom = "=0.2.16" +libc = "=0.2.180" libsqlite3-sys = { version = "=0.33.0", features = ["bundled"] } -thread_local = "=1.1.8" -getrandom = { version = "0.3.3", features = [ "std" ] } + +# HTTP stack (ported from the old roc_http crate). Pinned to match the +# previous workspace; `ring` is the rustls crypto backend (the musl +# cross-compile risk lives here — validated via the zig cc wrapper). +hyper = { version = "=1.6.0", default-features = false, features = ["http1", "client"] } +hyper-util = { version = "=0.1.12", default-features = false, features = ["client", "client-legacy", "http1", "tokio"] } +hyper-rustls = { version = "=0.27.6", default-features = false, features = ["http1", "tls12", "native-tokio", "rustls-native-certs", "ring"] } +http-body-util = "=0.1.3" +tokio = { version = "=1.45.0", default-features = false, features = ["rt", "net", "time"] } +bytes = "=1.10.1" + +[target.'cfg(not(target_os = "macos"))'.dependencies] +sys-locale = "=0.3.2" + +[profile.release] +lto = true +strip = "debuginfo" +panic = "abort" diff --git a/README.md b/README.md index a51182b9..8ed7a06d 100644 --- a/README.md +++ b/README.md @@ -5,29 +5,46 @@ # basic-cli -A Roc [platform](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform) to work with files, commands, HTTP, TCP, command line arguments,... +A Roc [platform](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform) for command-line programs. + +This migration branch supports command execution, directories, environment variables, files, locales, paths, random seeds, sleeping, standard input/output/error, terminal raw mode, and UTC time. HTTP, TCP, SQLite, and URL helpers from the old API have not been ported to the new compiler backend yet. :eyes: **examples**: - - [0.20.0](https://github.com/roc-lang/basic-cli/tree/0.20.0/examples) - - [0.19.0](https://github.com/roc-lang/basic-cli/tree/0.19.0/examples) - - [0.18.0](https://github.com/roc-lang/basic-cli/tree/0.18.0/examples) - [latest main branch](https://github.com/roc-lang/basic-cli/tree/main/examples) :book: **documentation**: - - [0.20.0](https://roc-lang.github.io/basic-cli/0.20.0/) - - [0.19.0](https://roc-lang.github.io/basic-cli/0.19.0/) - - [0.18.0](https://roc-lang.github.io/basic-cli/0.18.0/) - - [latest main branch](https://roc-lang.github.io/basic-cli/main/) + - TBA -- `roc docs` not yet implemented in the new compiler ## Running Locally -If you clone this repo instead of using the release URL you'll need to build the platform once: +This branch requires a Roc compiler matching the commit in `.roc-version`. + +### Version Requirements + +`ci/all_tests.sh` reads `.roc-version`, reuses `roc` on `PATH` when it matches, and otherwise builds the pinned Roc compiler into `roc-src/`. + +```sh +cat .roc-version +roc version +``` + +### Rust Glue + +The Rust host ABI is generated from `platform/main.roc` using Roc's `RustGlue.roc` generator: + ```sh -./jump-start.sh -roc build.roc --linker=legacy +./ci/regenerate_glue.sh +./ci/regenerate_glue.sh --check ``` -Then you can run like usual: + +Commit `src/roc_platform_abi.rs` with any platform API change and the matching Rust host updates. + +### Verification + +Run the full local check before opening release or CI-facing changes: + ```sh -$ roc examples/hello-world.roc -Hello, World! +./ci/all_tests.sh ``` + +The script checks generated glue, builds the host, checks and builds every example, and runs expect tests for examples with maintained scripts. When all target host libraries are present, it also bundles the platform, serves it from localhost, and tests examples against that bundle. diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md new file mode 100644 index 00000000..5a715587 --- /dev/null +++ b/THIRD_PARTY_LICENSES.md @@ -0,0 +1,241 @@ +# Third-Party Licenses + +This project bundles third-party libraries for static linking on Linux (musl) targets. + +## musl libc + +Files: `platform/targets/*/libc.a`, `platform/targets/*/crt1.o` + +musl libc is licensed under the MIT License. + +Source: https://musl.libc.org/ + +``` +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +## LLVM libunwind + +Files: `platform/targets/*/libunwind.a` + +LLVM libunwind is part of the LLVM Project and is licensed under the Apache License v2.0 with LLVM Exceptions. + +Source: https://github.com/llvm/llvm-project/tree/main/libunwind + +``` +============================================================================== +The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. +``` diff --git a/basic-cli-build-steps.png b/basic-cli-build-steps.png deleted file mode 100644 index 43a023b0..00000000 Binary files a/basic-cli-build-steps.png and /dev/null differ diff --git a/build.roc b/build.roc deleted file mode 100755 index a38eb01a..00000000 --- a/build.roc +++ /dev/null @@ -1,154 +0,0 @@ -app [main!] { - cli: platform "platform/main.roc", -} - -import cli.Cmd -import cli.Stdout -import cli.Env - -## Builds the basic-cli [platform](https://www.roc-lang.org/platforms). -## -## run with: roc ./build.roc -## -## Check basic-cli-build-steps.png for a diagram that shows what the code does. -## -main! : _ => Result {} _ -main! = |_args| - - roc_cmd = Env.var!("ROC") |> Result.with_default("roc") - - debug_mode = - when Env.var!("DEBUG") is - Ok(str) if !(Str.is_empty(str)) -> Debug - _ -> Release - - roc_version!(roc_cmd)? - - os_and_arch = get_os_and_arch!({})? - - stub_lib_path = "platform/libapp.${stub_file_extension(os_and_arch)}" - - build_stub_app_lib!(roc_cmd, stub_lib_path)? - - cargo_build_host!(debug_mode)? - - rust_target_folder = get_rust_target_folder!(debug_mode)? - - copy_host_lib!(os_and_arch, rust_target_folder)? - - preprocess_host!(roc_cmd, stub_lib_path, rust_target_folder)? - - info!("Successfully built platform files!")? - - Ok({}) - -roc_version! : Str => Result {} _ -roc_version! = |roc_cmd| - info!("Checking provided roc; executing `${roc_cmd} version`:")? - - Cmd.exec!(roc_cmd, ["version"]) - -get_os_and_arch! : {} => Result OSAndArch _ -get_os_and_arch! = |{}| - info!("Getting the native operating system and architecture ...")? - - convert_os_and_arch!(Env.platform!({})) - -OSAndArch : [ - MacosArm64, - MacosX64, - LinuxArm64, - LinuxX64, - WindowsArm64, - WindowsX64, -] - -convert_os_and_arch! : _ => Result OSAndArch _ -convert_os_and_arch! = |{ os, arch }| - when (os, arch) is - (MACOS, AARCH64) -> Ok(MacosArm64) - (MACOS, X64) -> Ok(MacosX64) - (LINUX, AARCH64) -> Ok(LinuxArm64) - (LINUX, X64) -> Ok(LinuxX64) - _ -> Err(UnsupportedNative(os, arch)) - -build_stub_app_lib! : Str, Str => Result {} _ -build_stub_app_lib! = |roc_cmd, stub_lib_path| - info!("Building stubbed app shared library ...")? - - Cmd.exec!(roc_cmd, ["build", "--lib", "platform/libapp.roc", "--output", stub_lib_path, "--optimize"]) - -stub_file_extension : OSAndArch -> Str -stub_file_extension = |os_and_arch| - when os_and_arch is - MacosX64 | MacosArm64 -> "dylib" - LinuxArm64 | LinuxX64 -> "so" - WindowsX64 | WindowsArm64 -> "dll" - -prebuilt_static_lib_file : OSAndArch -> Str -prebuilt_static_lib_file = |os_and_arch| - when os_and_arch is - MacosArm64 -> "macos-arm64.a" - MacosX64 -> "macos-x64.a" - LinuxArm64 -> "linux-arm64.a" - LinuxX64 -> "linux-x64.a" - WindowsArm64 -> "windows-arm64.lib" - WindowsX64 -> "windows-x64.lib" - -get_rust_target_folder! : [Debug, Release] => Result Str _ -get_rust_target_folder! = |debug_mode| - - debug_or_release = if debug_mode == Debug then "debug" else "release" - - when Env.var!("CARGO_BUILD_TARGET") is - Ok(target_env_var) -> - if Str.is_empty(target_env_var) then - Ok("target/${debug_or_release}/") - else - Ok("target/${target_env_var}/${debug_or_release}/") - - Err(e) -> - info!("Failed to get env var CARGO_BUILD_TARGET with error ${Inspect.to_str(e)}. Assuming default CARGO_BUILD_TARGET (native)...")? - - Ok("target/${debug_or_release}/") - -cargo_build_host! : [Debug, Release] => Result {} _ -cargo_build_host! = |debug_mode| - - cargo_build_args! = |{}| - when debug_mode is - Debug -> - info!("Building rust host in debug mode...")? - Ok(["build"]) - - Release -> - info!("Building rust host ...")? - Ok(["build", "--release"]) - - args = cargo_build_args!({})? - - Cmd.exec!("cargo", args) - -copy_host_lib! : OSAndArch, Str => Result {} _ -copy_host_lib! = |os_and_arch, rust_target_folder| - - host_build_path = "${rust_target_folder}libhost.a" - - host_dest_path = "platform/${prebuilt_static_lib_file(os_and_arch)}" - - info!("Moving the prebuilt binary from ${host_build_path} to ${host_dest_path} ...")? - - Cmd.exec!("cp", [host_build_path, host_dest_path]) - -preprocess_host! : Str, Str, Str => Result {} _ -preprocess_host! = |roc_cmd, stub_lib_path, rust_target_folder| - - info!("Preprocessing surgical host ...")? - - surgical_build_path = "${rust_target_folder}host" - - Cmd.exec!(roc_cmd, ["preprocess-host", surgical_build_path, "platform/main.roc", stub_lib_path]) - -info! : Str => Result {} _ -info! = |msg| - Stdout.line!("\u(001b)[34mINFO:\u(001b)[0m ${msg}") diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..493f7dcc --- /dev/null +++ b/build.sh @@ -0,0 +1,117 @@ +#!/bin/bash +set -eo pipefail + +# Build script for basic-cli platform +# Usage: +# ./build.sh - Build for native target only +# ./build.sh --all - Build for all targets (cross-compilation) + +# Get rust triple for a target name +get_rust_triple() { + case "$1" in + x64mac) echo "x86_64-apple-darwin" ;; + arm64mac) echo "aarch64-apple-darwin" ;; + x64musl) echo "x86_64-unknown-linux-musl" ;; + arm64musl) echo "aarch64-unknown-linux-musl" ;; + *) echo "Unknown target: $1" >&2; exit 1 ;; + esac +} + + +# All supported targets +ALL_TARGETS="x64mac arm64mac x64musl arm64musl" + +# Detect native target based on current platform +detect_native_target() { + local arch=$(uname -m) + local os=$(uname -s) + + if [ "$os" = "Darwin" ]; then + if [ "$arch" = "arm64" ]; then + echo "arm64mac" + else + echo "x64mac" + fi + elif [ "$os" = "Linux" ]; then + if [ "$arch" = "aarch64" ]; then + echo "arm64musl" + else + echo "x64musl" + fi + else + echo "Unsupported OS: $os" >&2 + exit 1 + fi +} + +# Build for a specific target (cross-compile) +build_target_cross() { + local target_name=$1 + local rust_triple=$(get_rust_triple "$target_name") + + echo "Building for $target_name ($rust_triple)..." + cargo build --release --lib --target "$rust_triple" + + mkdir -p "platform/targets/$target_name" + cp "target/$rust_triple/release/libhost.a" "platform/targets/$target_name/" + echo " -> platform/targets/$target_name/libhost.a" +} + +# Build for native target +# On macOS: no --target needed (native is fine) +# On Linux: must use --target for musl, since default is glibc +build_target_native() { + local target_name=$1 + local rust_triple=$(get_rust_triple "$target_name") + + echo "Building for $target_name (native)..." + + # On macOS, native build is fine + # On Linux, we must explicitly target musl (default is glibc) + if [[ "$target_name" == *"musl"* ]]; then + # Linux: need explicit musl target + rustup target add "$rust_triple" 2>/dev/null || true + cargo build --release --lib --target "$rust_triple" + mkdir -p "platform/targets/$target_name" + cp "target/$rust_triple/release/libhost.a" "platform/targets/$target_name/" + else + # macOS: native is fine + cargo build --release --lib + mkdir -p "platform/targets/$target_name" + cp "target/release/libhost.a" "platform/targets/$target_name/" + fi + + echo " -> platform/targets/$target_name/libhost.a" +} + +# Main logic +if [ "${1:-}" = "--all" ]; then + echo "Building for all targets..." + echo "" + + # Ensure all rust targets are installed + echo "Installing Rust targets..." + for target_name in $ALL_TARGETS; do + rust_triple=$(get_rust_triple "$target_name") + rustup target add "$rust_triple" 2>/dev/null || true + done + echo "" + + # Build each target (cross-compile) + for target_name in $ALL_TARGETS; do + build_target_cross "$target_name" + echo "" + done + + echo "All targets built successfully!" +else + # Build for native target only (no cross-compile) + TARGET=$(detect_native_target) + echo "Building for native target: $TARGET" + echo "" + + build_target_native "$TARGET" + + echo "" + echo "Build complete!" +fi diff --git a/bundle.sh b/bundle.sh new file mode 100755 index 00000000..81794fde --- /dev/null +++ b/bundle.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir="$(cd "$(dirname "$0")" && pwd)" +cd "$root_dir/platform" + +# Collect all .roc files +roc_files=(*.roc) + +# Collect all host libraries and runtime files from targets directories +lib_files=() +for lib in targets/*/*.a targets/*/*.o; do + if [[ -f "$lib" ]]; then + lib_files+=("$lib") + fi +done + +echo "Bundling ${#roc_files[@]} .roc files and ${#lib_files[@]} library files..." +echo "" +echo "Files to bundle:" +for f in "${roc_files[@]}"; do + echo " $f" +done +for f in "${lib_files[@]}"; do + echo " $f" +done +echo " THIRD_PARTY_LICENSES.md" +echo "" + +# Copy THIRD_PARTY_LICENSES.md into platform dir (roc bundle doesn't allow .. paths) +cp "$root_dir/THIRD_PARTY_LICENSES.md" . +trap 'rm -f THIRD_PARTY_LICENSES.md' EXIT + +roc bundle "${roc_files[@]}" "${lib_files[@]}" THIRD_PARTY_LICENSES.md --output-dir "$root_dir" "$@" diff --git a/ci/all_tests.sh b/ci/all_tests.sh index 2d739e1a..2c4ea349 100755 --- a/ci/all_tests.sh +++ b/ci/all_tests.sh @@ -1,192 +1,273 @@ #!/usr/bin/env bash +set -euo pipefail -# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ -set -exo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" -if [ -z "${EXAMPLES_DIR}" ]; then - echo "ERROR: The EXAMPLES_DIR environment variable is not set." >&2 +cd "$ROOT_DIR" - exit 1 -else - EXAMPLES_DIR=$(realpath "${EXAMPLES_DIR}")/ -fi +EXAMPLE_NAMES=() -if [ -z "${ROC}" ]; then - echo "ERROR: The ROC environment variable is not set. - Set it to something like: - /home/username/Downloads/roc_nightly-linux_x86_64-2023-10-30-cb00cfb/roc - or - /home/username/gitrepos/roc/target/build/release/roc" >&2 - - exit 1 -fi +# Cleanup function to restore examples and stop HTTP server +cleanup() { + echo "" + echo "=== Cleaning up ===" -TESTS_DIR="$(dirname "$EXAMPLES_DIR")/tests/" -export TESTS_DIR - -if [ "$NO_BUILD" != "1" ]; then - # May be needed for breaking roc changes. Also replace platform in build.roc with `cli: platform "platform/main.roc",` - ./jump-start.sh - - # build the basic-cli platform - $ROC dev ./build.roc --linker=legacy -- --roc $ROC -fi - -# roc check -for roc_file in $EXAMPLES_DIR*.roc; do - $ROC check $roc_file -done -for roc_file in $TESTS_DIR*.roc; do - $ROC check $roc_file -done - -$ROC ci/check_all_exposed_funs_tested.roc -$ROC ci/check_cargo_versions_match.roc + # Restore examples from backups + for f in examples/*.roc.bak; do + if [ -f "$f" ]; then + mv "$f" "${f%.bak}" + fi + done -# roc build -architecture=$(uname -m) + # Stop HTTP server if running + if [ -n "${HTTP_SERVER_PID:-}" ]; then + kill "$HTTP_SERVER_PID" 2>/dev/null || true + fi -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") + # Remove built binaries + for example in "${EXAMPLE_NAMES[@]}"; do + rm -f "examples/${example}" + done - if [ "$base_file" == "temp-dir.roc" ]; then - $ROC build $roc_file $ROC_BUILD_FLAGS --linker=legacy + # Remove bundle file + if [ -n "${BUNDLE_FILE:-}" ] && [ -f "$BUNDLE_FILE" ]; then + rm -f "$BUNDLE_FILE" + fi +} + +# Set up trap to ensure cleanup runs on exit +trap cleanup EXIT + +# Get the roc commit pinned in .roc-version +ROC_COMMIT=$(python3 ci/get_roc_commit.py) +ROC_COMMIT_SHORT="${ROC_COMMIT:0:8}" +NEED_BUILD=true +USE_ROC_SRC=false + +echo "=== basic-cli CI ===" +echo "" + +# Check if roc is already on PATH and matches pinned commit +if command -v roc &>/dev/null; then + SYSTEM_VERSION=$(roc version 2>/dev/null || echo "unknown") + if echo "$SYSTEM_VERSION" | grep -q "$ROC_COMMIT_SHORT"; then + echo "roc on PATH matches pinned commit: $SYSTEM_VERSION" + NEED_BUILD=false else - $ROC build $roc_file $ROC_BUILD_FLAGS + echo "roc on PATH ($SYSTEM_VERSION) doesn't match pinned commit ($ROC_COMMIT_SHORT)" fi +fi -done -for roc_file in $TESTS_DIR*.roc; do - $ROC build $roc_file $ROC_BUILD_FLAGS -done - -# Check for duplicate .roc file names between EXAMPLES_DIR and TESTS_DIR (this messes with checks) -for example_file in $EXAMPLES_DIR*.roc; do - example_basename=$(basename "$example_file") - if [ -f "$TESTS_DIR$example_basename" ]; then - echo "ERROR: Duplicate file name found: $example_basename exists in both $EXAMPLES_DIR and $TESTS_DIR. Change the name of one of them." >&2 - exit 1 +# Check cached build in roc-src/ +if [ "$NEED_BUILD" = true ] && [ -d "roc-src" ] && [ -f "roc-src/zig-out/bin/roc" ]; then + CACHED_VERSION=$(./roc-src/zig-out/bin/roc version 2>/dev/null || echo "unknown") + if echo "$CACHED_VERSION" | grep -q "$ROC_COMMIT_SHORT"; then + echo "roc in roc-src/ matches pinned commit: $CACHED_VERSION" + NEED_BUILD=false + USE_ROC_SRC=true + else + echo "Cached roc ($CACHED_VERSION) doesn't match pinned commit ($ROC_COMMIT_SHORT)" + echo "Removing stale roc-src..." + rm -rf roc-src fi -done +fi -# prep for next step -cd ci/rust_http_server -cargo build --release -cd ../.. +if [ "$NEED_BUILD" = true ]; then + ROC_COMMIT="$ROC_COMMIT" ./ci/build_pinned_roc.sh + USE_ROC_SRC=true +fi -# check output with linux expect -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") +# Prefer the cached/source-built roc if it exists; otherwise keep the matching PATH roc. +if [ "$USE_ROC_SRC" = true ]; then + export PATH="$(pwd)/roc-src/zig-out/bin:$PATH" +fi - # Skip env-var.roc when on aarch64 - if [ "$architecture" == "aarch64" ] && [ "$base_file" == "env-var.roc" ]; then - continue - fi +echo "" +echo "Using roc version: $(roc version)" - ## Skip file-accessed-modified-created-time.roc when IS_MUSL=1 - if [ "$IS_MUSL" == "1" ] && [ "$base_file" == "file-accessed-modified-created-time.roc" ]; then - continue +if [ "$(uname -s)" = "Darwin" ] && [ -z "${SDKROOT:-}" ]; then + SDKROOT=$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true) + if [ -n "$SDKROOT" ]; then + export SDKROOT + echo "Using SDKROOT: $SDKROOT" fi +fi - roc_file_only="$(basename "$roc_file")" - no_ext_name=${roc_file_only%.*} +echo "" +echo "=== Checking generated Rust glue ===" +./ci/regenerate_glue.sh --check - # not used, leaving here for future reference if we want to run valgrind on an example - # if [ "$no_ext_name" == "args" ] && command -v valgrind &> /dev/null; then - # valgrind $EXAMPLES_DIR/args argument - # fi +# Build the platform +if [ "${NO_BUILD:-}" != "1" ]; then + echo "" + echo "=== Building platform ===" + ./build.sh +else + echo "" + echo "=== Skipping platform build (NO_BUILD=1) ===" +fi - expect ci/expect_scripts/$no_ext_name.exp -done -for roc_file in $TESTS_DIR*.roc; do +EXAMPLES_DIR="${ROOT_DIR}/examples/" +export EXAMPLES_DIR - roc_file_only="$(basename "$roc_file")" - no_ext_name=${roc_file_only%.*} +TESTS_DIR="${ROOT_DIR}/tests/" +export TESTS_DIR - expect ci/expect_scripts/$no_ext_name.exp +# Examples with maintained expect tests. All examples are checked and built +# below; this list only controls which built binaries are executed. +EXPECT_EXAMPLES=( + "command-line-args" + "hello-world" + "stdin-basic" + "path" + "command" + "file-read-write" + "time" + "random" + "locale" + "tty" + "dir" + "env-var" + "sqlite-basic" + "tcp-client" + "http-client" +) + +for roc_file in "${EXAMPLES_DIR}"*.roc; do + [ -f "$roc_file" ] && EXAMPLE_NAMES+=("$(basename "${roc_file%.roc}")") done -# remove Dir example directorys if they exist -rm -rf dirExampleE -rm -rf dirExampleA -rm -rf dirExampleD - -# countdown, echo, form... all require user input or special setup -ignore_list=("stdin-basic.roc" "stdin-pipe.roc" "command-line-args.roc" "http.roc" "env-var.roc" "bytes-stdin-stdout.roc" "error-handling.roc" "tcp-client.roc" "tcp.roc" "terminal-app-snake.roc") - -# roc dev (some expects only run with `roc dev`) -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") - - - # check if base_file matches something from ignore_list - for file in "${ignore_list[@]}"; do - if [ "$base_file" == "$file" ]; then - continue 2 # continue the outer loop if a match is found - fi - done - - # For path.roc we need be inside the EXAMPLES_DIR - if [ "$base_file" == "path.roc" ]; then - absolute_roc=$(which $ROC | xargs realpath) - cd $EXAMPLES_DIR - $absolute_roc dev $base_file $ROC_BUILD_FLAGS - cd .. - elif [ "$base_file" == "sqlite-basic.roc" ]; then - DB_PATH=${EXAMPLES_DIR}todos.db $ROC dev $roc_file $ROC_BUILD_FLAGS - elif [ "$base_file" == "sqlite-everything.roc" ]; then - DB_PATH=${EXAMPLES_DIR}todos2.db $ROC dev $roc_file $ROC_BUILD_FLAGS - elif [ "$base_file" == "temp-dir.roc" ]; then - $ROC dev $roc_file $ROC_BUILD_FLAGS --linker=legacy - elif [ "$base_file" == "file-accessed-modified-created-time.roc" ] && [ "$IS_MUSL" == "1" ]; then - continue - else - - $ROC dev $roc_file $ROC_BUILD_FLAGS +# Check if all target libraries exist for bundling +ALL_TARGETS_EXIST=true +for target in x64mac arm64mac x64musl arm64musl; do + if [ ! -f "platform/targets/$target/libhost.a" ]; then + ALL_TARGETS_EXIST=false + break fi done -for roc_file in $TESTS_DIR*.roc; do - base_file=$(basename "$roc_file") - - # check if base_file matches something from ignore_list - for file in "${ignore_list[@]}"; do - if [ "$base_file" == "$file" ]; then - continue 2 # continue the outer loop if a match is found +# Bundle and set up HTTP server if all targets exist +BUNDLE_FILE="" +HTTP_SERVER_PID="" +USE_BUNDLE=false + +if [ "${NO_BUNDLE:-}" = "1" ]; then + echo "" + echo "=== Skipping bundle (NO_BUNDLE=1) ===" +elif [ "$ALL_TARGETS_EXIST" = true ]; then + echo "" + echo "=== Bundling platform ===" + BUNDLE_OUTPUT=$(./bundle.sh 2>&1) + echo "$BUNDLE_OUTPUT" + + # Extract bundle filename from output + BUNDLE_PATH=$(echo "$BUNDLE_OUTPUT" | grep "^Created:" | awk '{print $2}') + BUNDLE_FILE=$(basename "$BUNDLE_PATH") + + if [ -n "$BUNDLE_FILE" ] && [ -f "$BUNDLE_FILE" ]; then + echo "" + echo "=== Starting HTTP server for bundle testing ===" + python3 -m http.server 8000 & + HTTP_SERVER_PID=$! + sleep 2 + + # Verify server is running + if curl -f -I "http://localhost:8000/$BUNDLE_FILE" > /dev/null 2>&1; then + echo "HTTP server running at http://localhost:8000" + echo "Bundle: $BUNDLE_FILE" + + # Modify examples to use bundle URL + echo "" + echo "=== Configuring examples to use bundle ===" + for example in examples/*.roc; do + sed -i.bak "s|platform \"../platform/main.roc\"|platform \"http://localhost:8000/$BUNDLE_FILE\"|" "$example" + done + USE_BUNDLE=true + else + echo "Warning: HTTP server failed to start, testing with local platform" + kill "$HTTP_SERVER_PID" 2>/dev/null || true + HTTP_SERVER_PID="" fi - done - - if [ "$base_file" == "sqlite.roc" ]; then - DB_PATH=${TESTS_DIR}test.db $ROC dev $roc_file $ROC_BUILD_FLAGS else - $ROC dev $roc_file $ROC_BUILD_FLAGS + echo "Warning: Bundle creation failed, testing with local platform" fi +else + echo "" + echo "=== Skipping bundle (not all targets built) ===" + echo "Run './build.sh --all' first to test with bundled platform" +fi + +echo "" +echo "=== Checking examples ===" +for example in "${EXAMPLE_NAMES[@]}"; do + echo "Checking: ${example}.roc" + roc check "examples/${example}.roc" done -# remove Dir example directorys if they exist -rm -rf dirExampleE -rm -rf dirExampleA -rm -rf dirExampleD +TESTS_FILES=() +for roc_file in "${TESTS_DIR}"*.roc; do + [ -f "$roc_file" ] && TESTS_FILES+=("$(basename "${roc_file%.roc}")") +done -# `roc test` every roc file if it contains a test, skip roc_nightly folder -find . -type d -name "roc_nightly" -prune -o -type f -name "*.roc" -print | while read file; do - # Arg/*.roc hits github.com/roc-lang/roc/issues/5701 - if ! [[ "$file" =~ Arg/[A-Z][a-zA-Z0-9]*.roc ]]; then +echo "" +echo "=== Checking tests ===" +for test in "${TESTS_FILES[@]}"; do + echo "Checking: ${test}.roc" + roc check "tests/${test}.roc" +done - if grep -qE '^\s*expect(\s+|$)' "$file"; then +echo "" +if [ "$USE_BUNDLE" = true ]; then + echo "=== Building examples (using bundle) ===" +else + echo "=== Building examples (using local platform) ===" +fi +for example in "${EXAMPLE_NAMES[@]}"; do + echo "Building: ${example}.roc" + roc build "examples/${example}.roc" + mv "./${example}" "examples/" +done - # don't exit script if test_command fails - set +e - test_command=$($ROC test "$file") - test_exit_code=$? - set -e +# The http-client expect test drives a local HTTP server; build it up front. +if printf '%s\n' "${EXPECT_EXAMPLES[@]}" | grep -qx "http-client"; then + echo "" + echo "=== Building HTTP test server ===" + (cd ci/rust_http_server && cargo build --release) +fi - if [[ $test_exit_code -ne 0 && $test_exit_code -ne 2 ]]; then - exit $test_exit_code - fi - fi +# Run expect tests +echo "" +echo "=== Running expect tests ===" +FAILED=0 +for example in "${EXPECT_EXAMPLES[@]}"; do + echo "" + echo "--- Testing: $example ---" + if [ ! -f "ci/expect_scripts/${example}.exp" ]; then + echo "FAIL: missing expect script for $example" + FAILED=1 + continue + fi + set +e + expect "ci/expect_scripts/${example}.exp" + EXIT_CODE=$? + set -e + if [ $EXIT_CODE -eq 0 ]; then + echo "PASS: $example" + else + echo "FAIL: $example (exit code: $EXIT_CODE)" + FAILED=1 fi done -# test building website -$ROC docs platform/main.roc +echo "" +if [ $FAILED -eq 0 ]; then + if [ "$USE_BUNDLE" = true ]; then + echo "=== All tests passed (with bundle)! ===" + else + echo "=== All tests passed! ===" + fi +else + echo "=== Some tests failed ===" + exit 1 +fi diff --git a/ci/build_pinned_roc.sh b/ci/build_pinned_roc.sh new file mode 100755 index 00000000..e5ba113c --- /dev/null +++ b/ci/build_pinned_roc.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +ROC_COMMIT="${ROC_COMMIT:-$(python3 ci/get_roc_commit.py)}" +MAX_ATTEMPTS="${ROC_BUILD_RETRY_COUNT:-4}" +TRANSIENT_BUILD_ERROR='error: (unable|invalid HTTP response)|HttpConnectionClosing' + +echo "Building roc from pinned commit $ROC_COMMIT..." + +rm -rf roc-src +git init roc-src +cd roc-src +git remote add origin https://github.com/roc-lang/roc +git fetch --depth 1 origin "$ROC_COMMIT" +git checkout --detach "$ROC_COMMIT" + +attempt=1 +while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do + echo "Building roc attempt $attempt of $MAX_ATTEMPTS" + output_file="$(mktemp)" + + set +e + zig build roc > "$output_file" 2>&1 + status=$? + set -e + + cat "$output_file" + + if [ "$status" -eq 0 ]; then + rm -f "$output_file" + break + fi + + if [ "$attempt" -ge "$MAX_ATTEMPTS" ]; then + rm -f "$output_file" + exit "$status" + fi + + if grep -Eq "$TRANSIENT_BUILD_ERROR" "$output_file"; then + echo "Transient roc build failure; retrying..." + rm -f "$output_file" + attempt=$((attempt + 1)) + sleep 2 + else + rm -f "$output_file" + exit "$status" + fi +done + +if [ -n "${GITHUB_PATH:-}" ]; then + echo "$(pwd)/zig-out/bin" >> "$GITHUB_PATH" +fi diff --git a/ci/check_all_exposed_funs_tested.roc b/ci/check_all_exposed_funs_tested.roc deleted file mode 100644 index 126dbb3a..00000000 --- a/ci/check_all_exposed_funs_tested.roc +++ /dev/null @@ -1,342 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Stdout -import pf.Arg exposing [Arg] -import pf.File -import pf.Cmd -import pf.Env -import pf.Path -import pf.Sleep - -# This script performs the following tasks: -# 1. Reads the file platform/main.roc and extracts the exposes list. -# 2. For each module in the exposes list, it reads the corresponding file (e.g., platform/Path.roc) and extracts all functions in the module list. -# 3. Checks if each module.function is used in the examples or tests folder using ripgrep -# 4. Prints the functions that are not used anywhere in the examples or tests. - -## For convenient string errors -err_s = |err_msg| Err(StrErr(err_msg)) - -main! : List Arg => Result {} _ -main! = |_args| - # Check if ripgrep is installed - _ = Cmd.exec!("rg", ["--version"]) ? RipgrepNotInstalled - - cwd = Env.cwd!({}) ? FailedToGetCwd - Stdout.line!("Current working directory: ${Path.display(cwd)}")? - - path_to_platform_main = "platform/main.roc" - - main_content = - File.read_utf8!(path_to_platform_main)? - - exposed_modules = - extract_exposes_list(main_content)? - - # Uncomment for debugging - # Stdout.line!("Found exposed modules: ${Str.join_with(exposed_modules, ", ")}")? - - module_name_and_functions = List.map_try!( - exposed_modules, - |module_name| - process_module!(module_name), - )? - - tagged_functions = - module_name_and_functions - |> List.map_try!( - |{ module_name, exposed_functions }| - List.map_try!( - exposed_functions, - |function_name| - if is_function_unused!(module_name, function_name)? then - Ok(NotFound("${module_name}.${function_name}")) - else - Ok(Found("${module_name}.${function_name}")), - ), - )? - - not_found_functions = - tagged_functions - |> List.join - |> List.map( - |tagged_function| - when tagged_function is - Found _ -> "" - NotFound qualified_function_name -> qualified_function_name, - ) - |> List.keep_if(|s| !Str.is_empty(s)) - - if List.is_empty(not_found_functions) then - Ok({}) - else - Stdout.line!("Functions not used in basic-cli/examples or basic-cli/tests:")? - List.for_each_try!( - not_found_functions, - |function_name| - Stdout.line!(function_name), - )? - - # Sleep to fix print order - Sleep.millis!(1000) - Err(Exit(1, "I found untested functions, see above.")) - -is_function_unused! : Str, Str => Result Bool _ -is_function_unused! = |module_name, function_name| - function_pattern = "${module_name}.${function_name}" - search_dirs = ["examples", "tests"] - - # Check current working directory - cwd = Env.cwd!({}) ? FailedToGetCwd2 - - # Check if directories exist - List.for_each_try!( - search_dirs, - |search_dir| - is_dir_res = File.is_dir!(search_dir) - - when is_dir_res is - Ok is_dir -> - if !is_dir then - err_s("Error: Path '${search_dir}' inside ${Path.display(cwd)} is not a directory.") - else - Ok({}) - - Err (PathErr NotFound) -> - err_s("Error: Directory '${search_dir}' does not exist in ${Path.display(cwd)}") - Err err -> - err_s("Error checking directory '${search_dir}': ${Inspect.to_str(err)}") - )? - - - unused_in_dir = - search_dirs - |> List.map_try!( |search_dir| - # Skip searching if directory doesn't exist - dir_exists = File.is_dir!(search_dir)? - if !dir_exists then - Ok(Bool.true) # Consider unused if we can't search - else - # Use ripgrep to search for the function pattern - cmd_res = - Cmd.exec!("rg", ["-q", function_pattern, search_dir]) - - when cmd_res is - Ok(_) -> Ok(Bool.false) # Function is used (not unused) - _ -> Ok(Bool.true) - )? - - unused_in_dir - |> List.walk!(Bool.true, |state, is_unused_res| state && is_unused_res) - |> Ok - - - -process_module! : Str => Result { module_name : Str, exposed_functions : List Str } _ -process_module! = |module_name| - module_path = "platform/${module_name}.roc" - - module_source_code = - File.read_utf8!(module_path)? - - module_items = - extract_module_list(module_source_code)? - - module_functions = - module_items - |> List.keep_if(starts_with_lowercase) - - Ok({ module_name, exposed_functions: module_functions }) - -expect - input = - """ - exposes [ - Path, - File, - Http - ] - """ - - output = - extract_exposes_list(input) - - output == Ok(["Path", "File", "Http"]) - -# extra comma -expect - input = - """ - exposes [ - Path, - File, - Http, - ] - """ - - output = extract_exposes_list(input) - - output == Ok(["Path", "File", "Http"]) - -# single line -expect - input = "exposes [Path, File, Http]" - - output = extract_exposes_list(input) - - output == Ok(["Path", "File", "Http"]) - -# empty list -expect - input = "exposes []" - - output = extract_exposes_list(input) - - output == Ok([]) - -# multiple spaces -expect - input = "exposes [Path]" - - output = extract_exposes_list(input) - - output == Ok(["Path"]) - -extract_exposes_list : Str -> Result (List Str) _ -extract_exposes_list = |source_code| - - when Str.split_first(source_code, "exposes") is - Ok { after } -> - trimmed_after = Str.trim(after) - - if Str.starts_with(trimmed_after, "[") then - list_content = Str.replace_first(trimmed_after, "[", "") - - when Str.split_first(list_content, "]") is - Ok { before } -> - modules = - before - |> Str.split_on(",") - |> List.map(Str.trim) - |> List.keep_if(|s| !Str.is_empty(s)) - - Ok(modules) - - Err _ -> - err_s("Could not find closing bracket for exposes list in source code:\n\t${source_code}") - else - err_s("Could not find opening bracket after 'exposes' in source code:\n\t${source_code}") - - Err _ -> - err_s("Could not find exposes section in source_code:\n\t${source_code}") - -expect - input = - """ - module [ - Path, - display, - from_str, - IOErr - ] - """ - - output = extract_module_list(input) - - output == Ok(["Path", "display", "from_str", "IOErr"]) - -# extra comma -expect - input = - """ - module [ - Path, - display, - from_str, - IOErr, - ] - """ - - output = extract_module_list(input) - - output == Ok(["Path", "display", "from_str", "IOErr"]) - -expect - input = - "module [Path, display, from_str, IOErr]" - - output = extract_module_list(input) - - output == Ok(["Path", "display", "from_str", "IOErr"]) - -expect - input = - "module []" - - output = extract_module_list(input) - - output == Ok([]) - -# with extra space -expect - input = - "module [Path]" - - output = extract_module_list(input) - - output == Ok(["Path"]) - -extract_module_list : Str -> Result (List Str) _ -extract_module_list = |source_code| - - when Str.split_first(source_code, "module") is - Ok { after } -> - trimmed_after = Str.trim(after) - - if Str.starts_with(trimmed_after, "[") then - list_content = Str.replace_first(trimmed_after, "[", "") - - when Str.split_first(list_content, "]") is - Ok { before } -> - items = - before - |> Str.split_on(",") - |> List.map(Str.trim) - |> List.keep_if(|s| !Str.is_empty(s)) - - Ok(items) - - Err _ -> - err_s("Could not find closing bracket for module list in source code:\n\t${source_code}") - else - err_s("Could not find opening bracket after 'module' in source code:\n\t${source_code}") - - Err _ -> - err_s("Could not find module section in source_code:\n\t${source_code}") - -expect starts_with_lowercase("hello") == Bool.true -expect starts_with_lowercase("Hello") == Bool.false -expect starts_with_lowercase("!hello") == Bool.false -expect starts_with_lowercase("") == Bool.false - -starts_with_lowercase : Str -> Bool -starts_with_lowercase = |str| - if Str.is_empty(str) then - Bool.false - else - first_char_byte = - str - |> Str.to_utf8 - |> List.first - |> impossible_err("We verified that the string is not empty") - - # ASCII lowercase letters range from 97 ('a') to 122 ('z') - first_char_byte >= 97 and first_char_byte <= 122 - -impossible_err = |result, err_msg| - when result is - Ok something -> - something - - Err err -> - crash "This should have been impossible: ${err_msg}.\n\tError was: ${Inspect.to_str(err)}" diff --git a/ci/check_cargo_versions_match.roc b/ci/check_cargo_versions_match.roc deleted file mode 100644 index b6f8790a..00000000 --- a/ci/check_cargo_versions_match.roc +++ /dev/null @@ -1,350 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Stdout -import pf.Arg exposing [Arg] -import pf.File -import pf.Env -import pf.Path - -# This script performs the following tasks: -# 1. Reads Cargo.toml and ci/rust_http_server/Cargo.toml files -# 2. Extracts dependencies from both files -# 3. Compares versions of dependencies that exist in both files -# 4. Reports any version mismatches - -## For convenient string errors -err_s = |err_msg| Err(StrErr(err_msg)) - -err_exit = |err_msg| Err(Exit(1, "\n❌ ${err_msg}")) - -main! : List Arg => Result {} _ -main! = |_args| - cwd = Env.cwd!({}) ? FailedToGetCwd - Stdout.line!("Current working directory: ${Path.display(cwd)}")? - - root_cargo_path = "Cargo.toml" - ci_cargo_path = "ci/rust_http_server/Cargo.toml" - - # Check if both files exist - root_exists = File.exists!(root_cargo_path)? - ci_exists = File.exists!(ci_cargo_path)? - - if !root_exists then - err_exit("${root_cargo_path} not found in ${Path.display(cwd)}.") - else if !ci_exists then - err_exit("${ci_cargo_path} not found in ${Path.display(cwd)}.") - else - # Read both Cargo.toml files - root_content = File.read_utf8!(root_cargo_path)? - ci_content = File.read_utf8!(ci_cargo_path)? - - root_deps = extract_dependencies(root_content) - expect !List.is_empty(root_deps) - - ci_deps = extract_dependencies(ci_content) - expect !List.is_empty(ci_deps) - - mismatches = find_version_mismatches(root_deps, ci_deps) - - if List.is_empty(mismatches) then - Stdout.line!("✓ All shared dependencies have matching versions") - else - all_mistmatches_str = - mismatches - |> List.map( - |{ dep_name, root_version, ci_version }| - " ${dep_name}: ${root_cargo_path} has '${root_version}', ${ci_cargo_path} has '${ci_version}'" - ) - |> Str.join_with("\n") - - err_exit("Found version mismatches in shared dependencies:\n\n${all_mistmatches_str}") - -# test find_version_mismatches -expect - root_deps = [ - { name: "serde", version: "1.0" }, - { name: "tokio", version: "1.0" }, - { name: "unique_to_root", version: "2.0" }, - ] - - ci_deps = [ - { name: "serde", version: "1.0" }, - { name: "tokio", version: "1.0.1" }, - { name: "unique_to_ci", version: "3.0" }, - ] - - mismatches = find_version_mismatches(root_deps, ci_deps) - - expected = [ - { dep_name: "tokio", root_version: "1.0", ci_version: "1.0.1" }, - ] - - mismatches == expected - -find_version_mismatches : List { name : Str, version : Str }, List { name : Str, version : Str } -> List { dep_name : Str, root_version : Str, ci_version : Str } -find_version_mismatches = |root_deps, ci_deps| - root_deps - |> List.walk( - [], - |state, root_dep| - # Find matching dependency in CI cargo file - when List.find_first(ci_deps, |ci_dep| ci_dep.name == root_dep.name) is - Ok ci_dep -> - if root_dep.version != ci_dep.version then - List.append(state, { dep_name: root_dep.name, root_version: root_dep.version, ci_version: ci_dep.version }) - else - state # Versions match, no mismatch - - Err _ -> - state # Dependency not found in CI file, not a shared dependency - ) - -# test extract_dependencies -expect - input = - """ - [dependencies] - serde = { version = "1.0.0", features = ["derive"] } - tokio = { version = "1.0", default-features = false } - """ - - output = extract_dependencies(input) - - expected = [ - { name: "serde", version: "1.0.0" }, - { name: "tokio", version: "1.0" }, - ] - - output == expected - -extract_dependencies : Str -> (List { name : Str, version : Str }) -extract_dependencies = |toml_content| - lines = Str.split_on(toml_content, "\n") - - final_state = List.walk( - lines, - { deps: [], in_dep_section: Bool.false }, - |state, line| - trimmed_line = Str.trim(line) - - # Check if we're entering a dependency section - is_dep_section = Str.contains(trimmed_line, "dependencies]") - - # Check if we're entering a different section (starts with [ but not a dependency section) - is_other_section = - Str.starts_with(trimmed_line, "[") - && !is_dep_section - && !Str.is_empty(trimmed_line) - - new_in_dep_section = - if is_dep_section then - Bool.true - else if is_other_section then - Bool.false - else - state.in_dep_section - - if state.in_dep_section && !Str.is_empty(trimmed_line) && !Str.starts_with(trimmed_line, "[") then - # Parse dependency line - when parse_dependency_line(trimmed_line) is - Ok dep -> - { deps: List.append(state.deps, dep), in_dep_section: new_in_dep_section } - - Err _ -> - # Skip lines that don't parse as dependencies (like comments) - { state & in_dep_section: new_in_dep_section } - else - { state & in_dep_section: new_in_dep_section } - ) - - final_state.deps - - -# test parse_dependency_line -expect - input2 = "tokio = { version = \"1.0\", features = [\"full\"] }" - output2 = parse_dependency_line(input2) - output2 == Ok({ name: "tokio", version: "1.0" }) - -parse_dependency_line : Str -> Result { name : Str, version : Str } _ -parse_dependency_line = |line| - trimmed = Str.trim(line) - - # Skip comments and empty lines - if Str.starts_with(trimmed, "#") || Str.is_empty(trimmed) then - err_s("I expected a dependency line like 'dep = \"1.0\"', but got '${trimmed}'.") - else - { before: name_part, after: value_part } = Str.split_first(trimmed, "=") ? |_| err_s("I expected a `=` in this line: '${trimmed}'") - - dep_name = Str.trim(name_part) - trimmed_value = Str.trim(value_part) - - version = - if Str.starts_with(trimmed_value, "\"") then - # Simple version string like "1.0" - extract_quoted_string(trimmed_value)? - else if Str.starts_with(trimmed_value, "{") then - # Table format like { version = "1.0", features = [...] } - extract_version_from_table(trimmed_value)? - else - err_s("I don't recognize this dependency format: ${trimmed_value}")? - - Ok({ name: dep_name, version }) - -# test extract_quoted_string -expect - input1 = "\"1.0\"" - output1 = extract_quoted_string(input1) - output1 == Ok("1.0") - -extract_quoted_string : Str -> Result Str _ -extract_quoted_string = |str| - if Str.starts_with(str, "\"") && Str.ends_with(str, "\"") then - # Remove first and last character (the quotes) - inner = - str - |> Str.to_utf8 - |> List.drop_first(1) - |> List.drop_last(1) - - Str.from_utf8(inner) - else - err_s("String is not properly quoted: ${str}") - - -# test extract_version_from_table -expect - input2 = "{ version = \"1.0\", features = [\"full\"] }" - output2 = extract_version_from_table(input2) - output2 == Ok("1.0") - -extract_version_from_table : Str -> Result Str _ -extract_version_from_table = |table_str| - # Find "version = " in the table - { after } = Str.split_first(table_str, "version") ? |_| err_s("Could not find substring 'version' in table: ${table_str}") - - # Look for the equals sign - trimmed_after = Str.trim(after) - - if Str.starts_with(trimmed_after, "=") then - value_part = - trimmed_after - |> Str.to_utf8 - |> List.drop_first(1) - |> Str.from_utf8? - |> Str.trim - # Find the quoted version string - { after: after_first_quote } = Str.split_first(value_part, "\"") ? |_| err_s("Could not find opening quote for version in: ${table_str}") - { before: version_content } = Str.split_first(after_first_quote, "\"") ? |_| err_s("Could not find closing quote for version in: ${table_str}") - - Ok(version_content) - else - err_s("Could not find '=' after version in: ${table_str}") - -# START extra extract_dependencies tests -expect - input = - """ - [dependencies] - serde = "1.0" - tokio = { version = "1.0", features = ["full"] } - clap = "4.0" - - [dev-dependencies] - criterion = "0.5" - """ - - output = extract_dependencies(input) - - expected = [ - { name: "serde", version: "1.0" }, - { name: "tokio", version: "1.0" }, - { name: "clap", version: "4.0" }, - { name: "criterion", version: "0.5" }, - ] - - output == expected - -expect - input = - """ - [workspace.dependencies] - simple-dep = "1.0" - """ - - output = extract_dependencies(input) - - expected = [ - { name: "simple-dep", version: "1.0" }, - ] - - output == expected - -expect - input = "[dependencies]" - - output = extract_dependencies(input) - - output == [] - -# END extra extract_dependencies tests - -# START extra parse_dependency_line tests - -expect - input1 = "serde = \"1.0\"" - output1 = parse_dependency_line(input1) - output1 == Ok({ name: "serde", version: "1.0" }) - -expect - input3 = "clap = { version = \"4.0\" }" - output3 = parse_dependency_line(input3) - output3 == Ok({ name: "clap", version: "4.0" }) - -expect - input4 = "# this is a comment" - output4 = parse_dependency_line(input4) - when output4 is - Err _ -> Bool.true - Ok _ -> Bool.false - -expect - input5 = "invalid-line-without-equals" - output5 = parse_dependency_line(input5) - when output5 is - Err _ -> Bool.true - Ok _ -> Bool.false - -# END extra parse_dependency_line tests - -# START extra extract_quoted_string tests - -expect - input2 = "\"1.0.0\"" - output2 = extract_quoted_string(input2) - output2 == Ok("1.0.0") - -expect - input3 = "1.0" - output3 = extract_quoted_string(input3) - when output3 is - Err _ -> Bool.true - Ok _ -> Bool.false - -# END extra extract_quoted_string tests - -# START extra extract_version_from_table tests - -expect - input1 = "{ version = \"1.0\" }" - output1 = extract_version_from_table(input1) - output1 == Ok("1.0") - -expect - input3 = "{ features = [\"full\"] }" - output3 = extract_version_from_table(input3) - when output3 is - Err _ -> Bool.true - Ok _ -> Bool.false - -# END extra extract_version_from_table tests \ No newline at end of file diff --git a/ci/expect_scripts/cmd-test.exp b/ci/expect_scripts/cmd-test.exp deleted file mode 100644 index 23fee859..00000000 --- a/ci/expect_scripts/cmd-test.exp +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(TESTS_DIR)cmd-test - - -set expected_output [normalize_output { -cat: non_existent.txt: No such file or directory -cat: non_existent.txt: No such file or directory -cat: non_existent.txt: No such file or directory -All tests passed. -}] - -expect $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/command-line-args.exp b/ci/expect_scripts/command-line-args.exp index 074659d8..689ce004 100755 --- a/ci/expect_scripts/command-line-args.exp +++ b/ci/expect_scripts/command-line-args.exp @@ -5,18 +5,11 @@ set timeout 7 -spawn $env(EXAMPLES_DIR)/command-line-args foo - source ./ci/expect_scripts/shared-code.exp +spawn $env(EXAMPLES_DIR)/command-line-args foo -set expected_output [normalize_output { -received argument: foo -Unix argument, bytes: \[102, 111, 111\] -back to Arg: "foo" -}] - -expect $expected_output { +expect "received argument: foo\r\n" { expect eof { check_exit_and_segfault } diff --git a/ci/expect_scripts/command.exp b/ci/expect_scripts/command.exp index 59df35fb..abeafdd3 100644 --- a/ci/expect_scripts/command.exp +++ b/ci/expect_scripts/command.exp @@ -9,24 +9,23 @@ source ./ci/expect_scripts/shared-code.exp spawn $env(EXAMPLES_DIR)command - set expected_output [normalize_output { Hello -\{stderr_utf8_lossy: "", stdout_utf8: "Hi -"\} +{ stderr_utf8_lossy: "", stdout_utf8: "Hi +" } BAZ=DUCK FOO=BAR XYZ=ABC cat: non_existent.txt: No such file or directory Exit code: 1 -\{stderr_bytes: \[\], stdout_bytes: \[72, 105, 10\]\} +{ stderr_bytes: [], stdout_bytes: [72, 105, 10] } }] -expect $expected_output { +expect -exact $expected_output { expect eof { check_exit_and_segfault } } puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file +exit 1 diff --git a/ci/expect_scripts/env-var.exp b/ci/expect_scripts/env-var.exp index 63ae3710..01c24c9f 100644 --- a/ci/expect_scripts/env-var.exp +++ b/ci/expect_scripts/env-var.exp @@ -8,14 +8,12 @@ set timeout 7 source ./ci/expect_scripts/shared-code.exp set env(EDITOR) nano -set env(LETTERS) a,c,e,j spawn $env(EXAMPLES_DIR)env-var set expected_output [normalize_output { Your favorite editor is nano! -Your favorite letters are: a c e j }] expect $expected_output { diff --git a/ci/expect_scripts/env.exp b/ci/expect_scripts/env.exp deleted file mode 100644 index 975da4ea..00000000 --- a/ci/expect_scripts/env.exp +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(TESTS_DIR)env - -expect "Testing Env module functions..." { - expect "Testing Env.cwd!:" { - # Match cwd path that has ArbitraryBytes with non-empty list of integers - expect -re {cwd: /[^\r\n]*\r\n} { - - expect "Testing Env.exe_path!:" { - # Match exe_path with any valid format - expect -re {exe_path: /[^\r\n]*\r\n} { - - expect "Testing Env.platform!:" { - # Match platform info with any arch and OS - # Literal braces in regex need to be escaped: \{ and \} - expect -re {Current platform:\{arch: \w+, os: \w+\}} { - - expect "Testing Env.dict!:" { - # Match environment variables count as non-zero number - expect -re {Environment variables count: (\d+)} { - set env_count $expect_out(1,string) - if {$env_count < 1} { - puts stderr "\nExpect script failed: environment variable count is $env_count." - exit 1 - } - # Match sample environment variables with non-empty strings - # Literal brackets [], parentheses (), and quotes "" need escaping or careful handling - expect -re {Sample environment variables:\[\("\w+", ".*"\)(, \("\w+", ".*"\))*\]} { - - expect "Testing Env.set_cwd!:" { - # Match changed directory path with non-empty list of integers - expect -re {Changed current directory to: /[^\r\n]*\r\n} { - - expect "All tests executed." { - expect eof { - check_exit_and_segfault - } - } - } - } - } - } - } - } - } - } - } - } - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/error-handling.exp b/ci/expect_scripts/error-handling.exp deleted file mode 100644 index 25b9bc99..00000000 --- a/ci/expect_scripts/error-handling.exp +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)error-handling - -expect "NotFound\r\n" { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/file-accessed-modified-created-time.exp b/ci/expect_scripts/file-accessed-modified-created-time.exp deleted file mode 100644 index a63cd9dc..00000000 --- a/ci/expect_scripts/file-accessed-modified-created-time.exp +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)file-accessed-modified-created-time - -expect { - -re {LICENSE file time metadata:\r\n Modified: 20.*\r\n Accessed: 20.*\r\n Created: 20.*\r\n} { - expect eof { - check_exit_and_segfault - } - } - timeout { - puts stderr "\nExpect script failed: timed out waiting for expected output." - exit 1 - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/file-permissions.exp b/ci/expect_scripts/file-permissions.exp deleted file mode 100644 index 4c15e1cb..00000000 --- a/ci/expect_scripts/file-permissions.exp +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)file-permissions - -set expected_output [normalize_output { -LICENSE file permissions: - Executable: Bool.false - Readable: Bool.true - Writable: Bool.true -}] - -expect $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/file-read-buffered.exp b/ci/expect_scripts/file-read-buffered.exp deleted file mode 100644 index ab83a72e..00000000 --- a/ci/expect_scripts/file-read-buffered.exp +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)file-read-buffered - -expect "Done reading file: {bytes_read: 1915, lines_read: 17}\r\n" { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 diff --git a/ci/expect_scripts/file-size.exp b/ci/expect_scripts/file-size.exp deleted file mode 100644 index b92a4a61..00000000 --- a/ci/expect_scripts/file-size.exp +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)file-size - -expect "The size of the LICENSE file is: 1915 bytes\r\n" { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/file.exp b/ci/expect_scripts/file.exp deleted file mode 100644 index 66a0e077..00000000 --- a/ci/expect_scripts/file.exp +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(TESTS_DIR)file - -set expected_output [normalize_output " -Testing some File functions... -This will create and manipulate test files in the current directory. - -Testing File.write_bytes! and File.read_bytes!: -Bytes in test_bytes.txt: \\\[72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33\\\] - -Testing File.write!: -Content of test_write.json: {\"some\":\"json stuff\"} - -Testing File.is_file!: -✓ test_bytes.txt is confirmed to be a file - -Testing File.is_sym_link!: -✓ test_bytes.txt is not a symbolic link -✓ test_symlink.txt is a symbolic link - -Testing File.type!: -test_bytes.txt file type: IsFile -. file type: IsDir -test_symlink.txt file type: IsSymLink - -Testing File.open_reader_with_capacity!: -✓ Successfully opened reader with 3 byte capacity - -Reading lines from file: -Line 1: First line - -Line 2: Second line - - -Testing File.hard_link!: -✓ Successfully created hard link: test_link_to_original.txt -Hard link inodes should be equal: Bool.true -✓ Hard link contains same content as original - -Testing File.rename!: -✓ Successfully renamed test_rename_original.txt to test_rename_new.txt -✓ Original file test_rename_original.txt no longer exists -✓ Renamed file test_rename_new.txt exists -✓ Renamed file has correct content - -Testing File.exists!: -✓ File.exists! returns true for a file that exists -✓ File.exists! returns false for a file that does not exist - -I ran all file function tests. - -Cleaning up test files... -✓ Deleted all files. -"] - -expect -re $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/http-client.exp b/ci/expect_scripts/http-client.exp new file mode 100644 index 00000000..88d848b5 --- /dev/null +++ b/ci/expect_scripts/http-client.exp @@ -0,0 +1,26 @@ +#!/usr/bin/expect + +# uncomment line below for debugging +# exp_internal 1 + +set timeout 15 + +# Start the local test server (built by ci/all_tests.sh) in the background. +set server_pid [exec ./ci/rust_http_server/target/release/rust_http_server &] +sleep 2 + +spawn $env(EXAMPLES_DIR)http-client + +expect "I received 'Hello utf8' from the server.\r\n" { + expect "The json I received was: {\"foo\":\"Hello Json!\"}\r\n" { + expect "send! returned status 200.\r\n" { + exec kill $server_pid + exit 0 + } + } +} + +exec kill $server_pid + +puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." +exit 1 diff --git a/ci/expect_scripts/http.exp b/ci/expect_scripts/http.exp deleted file mode 100644 index d5cc2f0a..00000000 --- a/ci/expect_scripts/http.exp +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -# Start server to test with in the background and capture its process ID -set server_pid [exec ./ci/rust_http_server/target/release/rust_http_server &] -sleep 3 - -spawn $env(EXAMPLES_DIR)http - -expect "I received 'Hello utf8' from the server.\r\n" { - expect "The json I received was: { foo: \"Hello Json!\" }\r\n" { - - # we can kill our rust server now - exec kill $server_pid - - expect "\r\n" { - expect "\r\n" { - expect eof { - check_exit_and_segfault - } - } - } - } -} - -exec kill $server_pid - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/locale.exp b/ci/expect_scripts/locale.exp index 61b9eb64..ae6ca593 100644 --- a/ci/expect_scripts/locale.exp +++ b/ci/expect_scripts/locale.exp @@ -10,14 +10,13 @@ source ./ci/expect_scripts/shared-code.exp spawn $env(EXAMPLES_DIR)locale set expected_output [normalize_output { -The most preferred locale for this system or application: [a-zA-Z\-]+ +The most preferred locale for this system or application: [A-Za-z0-9_\-]+ All available locales for this system or application: \[.*\] }] expect -re $expected_output { - expect eof { - check_exit_and_segfault - } + expect eof { + check_exit_and_segfault } } diff --git a/ci/expect_scripts/path-test.exp b/ci/expect_scripts/path-test.exp deleted file mode 100755 index 5d3da4f8..00000000 --- a/ci/expect_scripts/path-test.exp +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(TESTS_DIR)path-test - -set expected_output [normalize_output " -Testing Path functions... -This will create and manipulate test files and directories in the current directory. - -Testing Path.from_bytes and Path.with_extension: -Created path from bytes: test_path -Path.from_bytes result matches expected: Bool.true -Path with extension: test_file.txt -Extension added correctly: Bool.true -Path with dot and extension: test_file.json -Extension after dot: Bool.true -Path with replaced extension: test_file.new -Extension replaced: Bool.true - -Testing Path file operations: -Bytes written: \\\[72, 101, 108, 108, 111, 44, 32, 80, 97, 116, 104, 33\\\] -Bytes read: \\\[72, 101, 108, 108, 111, 44, 32, 80, 97, 116, 104, 33\\\] -Bytes match: Bool.true -File content via cat: Hello from Path module! 🚀 -UTF-8 written: Hello from Path module! 🚀 -UTF-8 read: Hello from Path module! 🚀 -UTF-8 content matches: Bool.true -JSON content: {\"message\":\"Path test\",\"numbers\":\\\[1,2,3\\\]} -JSON contains 'message' field: Bool.true -JSON contains 'numbers' field: Bool.true -File no longer exists: Bool.true - -Testing Path directory operations... -Nested directory structure: -test_parent -test_parent/test_child -test_parent/test_child/test_grandchild - -Number of directories created: 3 -Directory contents: -total \\d+ -d.* \\. -d.* \\.\\. --.* file1\\.txt --.* file2\\.txt -d.* subdir - -Empty dir was deleted: Bool.true -Size before delete_all:\\s*\\d+\\w*\\s*test_parent - -Parent dir no longer exists: Bool.true - -Testing Path.hard_link!: -Hard link count before: 1 -Hard link count after: 2 -Original content: Original content for Path hard link test -Link content: Original content for Path hard link test -Content matches: Bool.true -Inode information: -\\d+ .* test_path_hardlink\\.txt -\\d+ .* test_path_original\\.txt - -First file inode: \\\[\"\\d+\"\\\] -Second file inode: \\\[\"\\d+\"\\\] -Inodes are equal: Bool.true - -Testing Path.rename!: -✓ Original file no longer exists -✓ Renamed file exists -✓ Renamed file has correct content - -Testing Path.exists!: -✓ Path.exists! returns true for a file that exists -✓ Path.exists! returns false for a file that does not exist - -I ran all Path function tests. - -Cleaning up test files... -Files to clean up: --.* test_path_bytes\\.txt --.* test_path_hardlink\\.txt --.* test_path_json\\.json --.* test_path_original\\.txt --.* test_path_rename_new\\.txt --.* test_path_utf8\\.txt - -ls: .* -ls: .* -ls: .* -ls: .* -ls: .* -ls: .* -Files deleted successfully: Bool.true -"] - -expect -re $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/path.exp b/ci/expect_scripts/path.exp index 2d165d26..af98ca99 100644 --- a/ci/expect_scripts/path.exp +++ b/ci/expect_scripts/path.exp @@ -11,10 +11,9 @@ cd $env(EXAMPLES_DIR) spawn ./path set expected_output [normalize_output { -is_file: Bool.true -is_dir: Bool.false -is_sym_link: Bool.false -type: IsFile +is_file: Ok(True) +is_dir: Ok(False) +is_sym_link: Ok(False) }] expect $expected_output { diff --git a/ci/expect_scripts/print.exp b/ci/expect_scripts/print.exp deleted file mode 100644 index 2c448326..00000000 --- a/ci/expect_scripts/print.exp +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)print - -set expected_output [normalize_output { -Hello, world! -No newline after me.Hello, error! -Err with no newline after.Foo -Bar -Baz -}] - -expect $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/random.exp b/ci/expect_scripts/random.exp index a6073fad..a13072f3 100644 --- a/ci/expect_scripts/random.exp +++ b/ci/expect_scripts/random.exp @@ -9,12 +9,7 @@ source ./ci/expect_scripts/shared-code.exp spawn $env(EXAMPLES_DIR)random -set expected_output [normalize_output " -Random U64 seed is: \\\d+ -Random U32 seed is: \\\d+ -"] - -expect -re $expected_output { +expect -re "Random U64 seed is: \[0-9\]+\r\n" { expect eof { check_exit_and_segfault } diff --git a/ci/expect_scripts/shared-code.exp b/ci/expect_scripts/shared-code.exp index cbdc14a4..27f56d98 100644 --- a/ci/expect_scripts/shared-code.exp +++ b/ci/expect_scripts/shared-code.exp @@ -21,4 +21,4 @@ proc check_exit_and_segfault {} { exit 0 } } -} \ No newline at end of file +} diff --git a/ci/expect_scripts/sqlite-basic.exp b/ci/expect_scripts/sqlite-basic.exp index 9f9107a8..484b6524 100644 --- a/ci/expect_scripts/sqlite-basic.exp +++ b/ci/expect_scripts/sqlite-basic.exp @@ -7,17 +7,17 @@ set timeout 7 source ./ci/expect_scripts/shared-code.exp -set env(DB_PATH) $env(EXAMPLES_DIR)todos.db +set env(DB_PATH) ./examples/todos.db spawn $env(EXAMPLES_DIR)sqlite-basic set expected_output [normalize_output { All Todos: - id: 3, task: Share my ❤️ for Roc, status: Todo + id: 3, task: Share my ❤️ for Roc, status: Todo Completed Todos: - id: 1, task: Prepare for AoC, status: Completed + id: 1, task: Prepare for AoC, status: Completed }] expect $expected_output { diff --git a/ci/expect_scripts/sqlite-everything.exp b/ci/expect_scripts/sqlite-everything.exp deleted file mode 100644 index c7942b56..00000000 --- a/ci/expect_scripts/sqlite-everything.exp +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -set env(DB_PATH) $env(EXAMPLES_DIR)todos2.db - -spawn $env(EXAMPLES_DIR)sqlite-everything - -set expected_output [normalize_output { -All Todos: - id: 1, task: Prepare for AoC, status: Completed, edited: Null - id: 2, task: Win all the Stars!, status: InProgress, edited: NotEdited - id: 3, task: Share my ❤️ for Roc, status: Todo, edited: NotEdited - -In-progress Todos: - In-progress tasks: Win all the Stars! - -Todos sorted by length of task description: - task: Prepare for AoC, status: Completed - task: Win all the Stars!, status: InProgress - task: Share my ❤️ for Roc, status: Todo -}] - -expect $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/sqlite.exp b/ci/expect_scripts/sqlite.exp deleted file mode 100644 index 318908a3..00000000 --- a/ci/expect_scripts/sqlite.exp +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -set env(DB_PATH) $env(TESTS_DIR)test.db - -spawn $env(TESTS_DIR)sqlite - -set expected_output [normalize_output " -Rows: {col_bytes: \\\[72, 101, 108, 108, 111\\\], col_f32: 78.9, col_f64: 123.456, col_i16: 1234, col_i32: 123456, col_i8: 123, col_nullable_bytes: (NotNull \\\[119, 111, 114, 108, 100\\\]), col_nullable_f32: (NotNull 12.34), col_nullable_f64: (NotNull 456.789), col_nullable_i16: (NotNull 5678), col_nullable_i32: (NotNull 456789), col_nullable_i64: (NotNull 987654321), col_nullable_i8: (NotNull 56), col_nullable_str: (NotNull \"nullable text\"), col_nullable_u16: (NotNull 8765), col_nullable_u32: (NotNull 987654), col_nullable_u64: (NotNull 123456789), col_nullable_u8: (NotNull 78), col_text: \"example text\", col_u16: 4321, col_u32: 654321, col_u8: 234} -{col_bytes: \\\[119, 111, 114, 108, 100\\\], col_f32: 23.45, col_f64: 456.789, col_i16: 5678, col_i32: 789012, col_i8: 45, col_nullable_bytes: Null, col_nullable_f32: (NotNull 67.89), col_nullable_f64: Null, col_nullable_i16: Null, col_nullable_i32: (NotNull 123456), col_nullable_i64: Null, col_nullable_i8: Null, col_nullable_str: Null, col_nullable_u16: Null, col_nullable_u32: (NotNull 654321), col_nullable_u64: Null, col_nullable_u8: Null, col_text: \"sample text\", col_u16: 9876, col_u32: 1234567, col_u8: 123} -Row count: 2 -Updated rows: \\\[\"Updated text 1\", \"Updated text 2\"\\\] -Tagged value test: \\\[(String \"example text\"), (String \"sample text\")\\\] -Error: Mismatch: Data type mismatch -Success! -"] - -expect $expected_output { - expect eof { - check_exit_and_segfault - } -} - - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/stdin-pipe.exp b/ci/expect_scripts/stdin-pipe.exp deleted file mode 100644 index c77ba633..00000000 --- a/ci/expect_scripts/stdin-pipe.exp +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -# -n to make sure Stdin.read_to_end! works without newline in input - -spawn bash -c "echo -n \"hey\" | $env(EXAMPLES_DIR)/stdin-pipe" - -expect "This is what you piped in: \"hey\"\r\n" { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/tcp-client.exp b/ci/expect_scripts/tcp-client.exp index e0495f2d..e44753d3 100644 --- a/ci/expect_scripts/tcp-client.exp +++ b/ci/expect_scripts/tcp-client.exp @@ -5,20 +5,24 @@ set timeout 7 -# get path to cat command -set cat_path [exec which cat] -# Start echo server -spawn ncat -e $cat_path -l 8085 +# Resolve the directory this script lives in so we can find the echo server. +set script_dir [file dirname [info script]] + +# Start a self-contained echo server (avoids depending on ncat/nc being present). +spawn python3 "$script_dir/tcp_echo_server.py" +set server_id $spawn_id sleep 1 spawn $env(EXAMPLES_DIR)tcp-client +set client_id $spawn_id - -expect "Connected!\r\n" { - expect "> " { - send -- "Hi\r" - expect "< Hi\r\n" { - exit 0 +expect { + -i $client_id "Connected!\r\n" { + expect -i $client_id "> " { + send -i $client_id -- "Hi\r" + expect -i $client_id "< Hi\r\n" { + exit 0 + } } } } diff --git a/ci/expect_scripts/tcp.exp b/ci/expect_scripts/tcp.exp deleted file mode 100644 index 56f74b63..00000000 --- a/ci/expect_scripts/tcp.exp +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -# get path to cat command -set cat_path [exec which cat] -# Start echo server -spawn ncat -e $cat_path -l 8085 -sleep 1 - -spawn $env(TESTS_DIR)tcp - - -set expected_output [normalize_output { -Testing Tcp module functions... -Note: These tests require a TCP server running on localhost:8085 -You can start one with: ncat -e `which cat` -l 8085 - -Testing Tcp.connect!: -✓ Successfully connected to localhost:8085 - -Testing Tcp.write!: -Echo server reply: Hello - - - -Testing Tcp.write_utf8!: -Echo server reply: Test message from Roc! - - - -Testing Tcp.read_up_to!: -Tcp.read_up_to yielded: 'do not read past me' - - -Testing Tcp.read_exactly!: -Tcp.read_exactly yielded: 'ABC' - - -Testing Tcp.read_until!: -Tcp.read_until yielded: 'Line1 -' - -All tests executed. -}] - -expect $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 diff --git a/ci/expect_scripts/tcp_echo_server.py b/ci/expect_scripts/tcp_echo_server.py new file mode 100644 index 00000000..cec6a6d1 --- /dev/null +++ b/ci/expect_scripts/tcp_echo_server.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Minimal single-connection TCP echo server used by tcp-client.exp. + +Listens on 127.0.0.1:8085, accepts one client, and echoes everything it +receives straight back until the client disconnects. This keeps the TCP +example's expect test self-contained (no dependency on `ncat`/`nc`). +""" +import socket + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(("127.0.0.1", 8085)) + server.listen(1) + conn, _ = server.accept() + with conn: + while True: + data = conn.recv(1024) + if not data: + break + conn.sendall(data) diff --git a/ci/expect_scripts/temp-dir.exp b/ci/expect_scripts/temp-dir.exp deleted file mode 100644 index b6b88ba6..00000000 --- a/ci/expect_scripts/temp-dir.exp +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)temp-dir - - -expect -re "The temp dir path is /\[^\r\n\]+\r\n" { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/terminal-app-snake.exp b/ci/expect_scripts/terminal-app-snake.exp deleted file mode 100644 index 12ad5c9c..00000000 --- a/ci/expect_scripts/terminal-app-snake.exp +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)terminal-app-snake - -expect "Score: 0\r\n" { - - # Press 's' key 9 times - for {set i 1} {$i <= 9} {incr i} { - send "s" - - expect -re {Score:.*} - } - - # This press should make the snake collide with the bottom wall and lead to game over - send "s" - - expect -re {.*Game Over.*} { - expect eof { - check_exit_and_segfault - } - } - -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/time.exp b/ci/expect_scripts/time.exp index 3912ed40..d372bf7d 100644 --- a/ci/expect_scripts/time.exp +++ b/ci/expect_scripts/time.exp @@ -9,7 +9,7 @@ source ./ci/expect_scripts/shared-code.exp spawn $env(EXAMPLES_DIR)time -expect -re "Completed in \[0-9]+ ns\r\n" { +expect -re "Completed in \[0-9\]+ ms \\(\[0-9\]+ ns\\)\r\n" { expect eof { check_exit_and_segfault } diff --git a/ci/expect_scripts/bytes-stdin-stdout.exp b/ci/expect_scripts/tty.exp similarity index 59% rename from ci/expect_scripts/bytes-stdin-stdout.exp rename to ci/expect_scripts/tty.exp index e6a652c8..10a106df 100644 --- a/ci/expect_scripts/bytes-stdin-stdout.exp +++ b/ci/expect_scripts/tty.exp @@ -7,20 +7,15 @@ set timeout 7 source ./ci/expect_scripts/shared-code.exp -spawn $env(EXAMPLES_DIR)bytes-stdin-stdout +spawn $env(EXAMPLES_DIR)tty -send -- "someinput\r" - -set expected_output [normalize_output { -someinput -someinput -}] - -expect $expected_output { - expect eof { - check_exit_and_segfault +expect -re "Tty: enabling raw mode" { + expect -re "Tty: disabling raw mode" { + expect eof { + check_exit_and_segfault + } } } puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file +exit 1 diff --git a/ci/expect_scripts/url.exp b/ci/expect_scripts/url.exp deleted file mode 100644 index c5e7d0cc..00000000 --- a/ci/expect_scripts/url.exp +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(TESTS_DIR)url - -set expected_output [normalize_output " -Testing Url module functions... -Created URL: https://example.com -Testing Url.append: -URL with append: https://example.com/some%20stuff -URL with query and fragment, then appended path: https://example.com/stuff\\?search=blah#fragment -URL with multiple appended paths: https://example.com/things/stuff/more/etc/ -Testing Url.append_param: -URL with appended param: https://example.com\\?email=someone%40example.com -URL with multiple appended params: https://example.com\\?caf%C3%A9=du%20Monde&email=hi%40example.com - -Testing Url.has_query: -URL with query has_query: Bool.true -URL without query has_query: Bool.false - -Testing Url.has_fragment: -URL with fragment has_fragment: Bool.true -URL without fragment has_fragment: Bool.false - -Testing Url.query: -Query from URL: key1=val1&key2=val2&key3=val3 -Query from URL without query: - -Testing Url.fragment: -Fragment from URL: stuff -Fragment from URL without fragment: - -Testing Url.reserve: -URL with reserved capacity and params: https://example.com/stuff\\?caf%C3%A9=du%20Monde&email=hi%40example.com - -Testing Url.with_query: -URL with replaced query: https://example.com\\?newQuery=thisRightHere#stuff -URL with removed query: https://example.com#stuff - -Testing Url.with_fragment: -URL with replaced fragment: https://example.com#things -URL with added fragment: https://example.com#things -URL with removed fragment: https://example.com - -Testing Url.query_params: -params_dict: {\"key1\": \"val1\", \"key2\": \"val2\", \"key3\": \"val3\"} - -Testing Url.path: -Path from URL: example.com/foo/bar -Path from relative URL: /foo/bar - -All tests executed. -"] - -expect -re $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/utc.exp b/ci/expect_scripts/utc.exp deleted file mode 100644 index 8ea5322e..00000000 --- a/ci/expect_scripts/utc.exp +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(TESTS_DIR)utc - -set expected_output [normalize_output " -Current time in milliseconds since epoch: \[0-9\]+ -Time reconstructed from milliseconds: \[0-9\]{4}-\[0-9\]{2}-\[0-9\]{2}T\[0-9\]{2}:\[0-9\]{2}:\[0-9\]{2}Z -Current time in nanoseconds since epoch: \[0-9\]+ -Time reconstructed from nanoseconds: \[0-9\]{4}-\[0-9\]{2}-\[0-9\]{2}T\[0-9\]{2}:\[0-9\]{2}:\[0-9\]{2}Z - -Time delta demonstration: -Starting time: \[0-9\]{4}-\[0-9\]{2}-\[0-9\]{2}T\[0-9\]{2}:\[0-9\]{2}:\[0-9\]{2}Z -Ending time: \[0-9\]{4}-\[0-9\]{2}-\[0-9\]{2}T\[0-9\]{2}:\[0-9\]{2}:\[0-9\]{2}Z -Time elapsed: \[0-9\]+ milliseconds -Time elapsed: \[0-9\]+ nanoseconds -Nanoseconds converted to milliseconds: \[0-9\]+\.\[0-9\]+ -Verified: deltaMillis and deltaNanos/1_000_000 match within tolerance - -All tests executed. -"] - -expect -re $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/get_latest_release_git_files.sh b/ci/get_latest_release_git_files.sh deleted file mode 100755 index b1a7a83d..00000000 --- a/ci/get_latest_release_git_files.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ -set -euxo pipefail - -mkdir latest-basic-cli-src && cd latest-basic-cli-src - -# get basic-cli -git clone --depth 1 https://github.com/roc-lang/basic-cli - -cd basic-cli - -# Fetch all tags -git fetch --tags - -# Get the latest tag matching pattern X.Y* -latestTag=$(git for-each-ref --sort=-version:refname --format '%(refname:short)' refs/tags/ | grep -E '^[0-9]+\.[0-9]+.*' | head -n1) - -# Checkout the latest tag -git checkout $latestTag - -mv * ../../ - -cd ../.. diff --git a/ci/get_roc_commit.py b/ci/get_roc_commit.py new file mode 100755 index 00000000..2991661e --- /dev/null +++ b/ci/get_roc_commit.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Read the roc commit hash from .roc-version.""" + +from pathlib import Path + + +def main() -> None: + version_path = Path(__file__).resolve().parent.parent / ".roc-version" + try: + commit = version_path.read_text().strip() + except FileNotFoundError: + raise SystemExit(f"Missing .roc-version at {version_path}") + + if not commit: + raise SystemExit(".roc-version is empty") + + print(commit) + + +if __name__ == "__main__": + main() diff --git a/ci/regenerate_glue.sh b/ci/regenerate_glue.sh new file mode 100755 index 00000000..63c9812d --- /dev/null +++ b/ci/regenerate_glue.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +ROC_BIN="${ROC:-roc}" +PLATFORM_FILE="${PLATFORM_FILE:-platform/main.roc}" +GLUE_OUT_DIR="${GLUE_OUT_DIR:-src}" +MODE="write" + +usage() { + cat <<'EOF' +Usage: ci/regenerate_glue.sh [--check] + +Regenerate Rust ABI bindings for the basic-cli Roc platform. + +Environment overrides: + ROC Roc executable to run. Default: roc + ROC_SRC Path to a Roc source checkout containing src/glue/src/RustGlue.roc + ROC_GLUE_SPEC Explicit path to RustGlue.roc + PLATFORM_FILE Platform file to analyze. Default: platform/main.roc + GLUE_OUT_DIR Output directory. Default: src + +By default the script looks for RustGlue.roc in ROC_GLUE_SPEC, ROC_SRC, +next to the ROC binary if it is from a source checkout, then sibling ../roc. +EOF +} + +if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then + usage + exit 0 +elif [ "${1:-}" = "--check" ]; then + MODE="check" +elif [ "${1:-}" != "" ]; then + usage >&2 + exit 2 +fi + +find_glue_spec() { + if [ -n "${ROC_GLUE_SPEC:-}" ]; then + echo "$ROC_GLUE_SPEC" + return 0 + fi + + candidates=() + + if [ -n "${ROC_SRC:-}" ]; then + candidates+=("${ROC_SRC%/}/src/glue/src/RustGlue.roc") + fi + + roc_path="$(command -v "$ROC_BIN" 2>/dev/null || true)" + if [ -n "$roc_path" ]; then + roc_bin_dir="$(cd "$(dirname "$roc_path")" 2>/dev/null && pwd || true)" + if [ -n "$roc_bin_dir" ]; then + roc_source_root="$(cd "$roc_bin_dir/../.." 2>/dev/null && pwd || true)" + if [ -n "$roc_source_root" ]; then + candidates+=("$roc_source_root/src/glue/src/RustGlue.roc") + fi + + if [ "$(basename "$roc_bin_dir")" = "bin" ] && [ "$(basename "$(dirname "$roc_bin_dir")")" = "zig-out" ]; then + roc_checkout_root="$(cd "$roc_bin_dir/../../.." 2>/dev/null && pwd || true)" + if [ -n "$roc_checkout_root" ]; then + candidates+=("$roc_checkout_root/src/glue/src/RustGlue.roc") + fi + fi + fi + fi + + candidates+=( + "../roc/src/glue/src/RustGlue.roc" + "../../roc/src/glue/src/RustGlue.roc" + ) + + for candidate in "${candidates[@]}"; do + if [ -f "$candidate" ]; then + echo "$candidate" + return 0 + fi + done + + echo "Could not find RustGlue.roc." >&2 + echo "Set ROC_SRC=/path/to/roc or ROC_GLUE_SPEC=/path/to/RustGlue.roc." >&2 + return 1 +} + +GLUE_SPEC="$(find_glue_spec)" + +if ! command -v "$ROC_BIN" >/dev/null 2>&1; then + echo "Could not find roc executable '$ROC_BIN'. Set ROC=/path/to/roc." >&2 + exit 1 +fi + +if [ ! -f "$PLATFORM_FILE" ]; then + echo "Platform file not found: $PLATFORM_FILE" >&2 + exit 1 +fi + +run_glue() { + local out_dir=$1 + mkdir -p "$out_dir" + "$ROC_BIN" glue "$GLUE_SPEC" "$out_dir" "$PLATFORM_FILE" +} + +if [ "$MODE" = "check" ]; then + tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/basic-cli-glue.XXXXXX")" + cleanup() { rm -rf "$tmp_dir"; } + trap cleanup EXIT + + run_glue "$tmp_dir" + + generated="$tmp_dir/roc_platform_abi.rs" + committed="$GLUE_OUT_DIR/roc_platform_abi.rs" + + if [ ! -f "$committed" ]; then + echo "Missing generated glue file: $committed" >&2 + exit 1 + fi + + if ! diff -u "$committed" "$generated"; then + echo "Generated Rust glue is stale. Run ci/regenerate_glue.sh and commit the result." >&2 + exit 1 + fi + + echo "Rust glue is up to date: $committed" +else + echo "Using roc: $ROC_BIN" + echo "Using glue spec: $GLUE_SPEC" + echo "Platform: $PLATFORM_FILE" + echo "Output dir: $GLUE_OUT_DIR" + run_glue "$GLUE_OUT_DIR" + echo "Generated: $GLUE_OUT_DIR/roc_platform_abi.rs" +fi diff --git a/ci/rust_http_server/Cargo.lock b/ci/rust_http_server/Cargo.lock index f5ccd48b..13e18d28 100644 --- a/ci/rust_http_server/Cargo.lock +++ b/ci/rust_http_server/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.11.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" diff --git a/ci/rust_http_server/Cargo.toml b/ci/rust_http_server/Cargo.toml index f9419241..b47449af 100644 --- a/ci/rust_http_server/Cargo.toml +++ b/ci/rust_http_server/Cargo.toml @@ -10,6 +10,6 @@ hyper = { version = "=1.6.0", default-features = false, features = ["server", "h tokio = { version = "=1.45.0", default-features = false, features = ["macros", "rt", "rt-multi-thread"] } hyper-util = { version = "=0.1.12", features = ["tokio"] } http-body-util = "=0.1.3" -bytes = "=1.11.1" +bytes = "=1.10.1" [workspace] diff --git a/ci/test_latest_release.sh b/ci/test_latest_release.sh deleted file mode 100755 index 0c038e09..00000000 --- a/ci/test_latest_release.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ -set -euxo pipefail - -if [ -z "${EXAMPLES_DIR}" ]; then - echo "ERROR: The EXAMPLES_DIR environment variable is not set." >&2 - exit 1 -fi - -# Install jq if it's not already available -command -v jq &>/dev/null || sudo apt install -y jq - -# Get the latest roc nightly -curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz - -# Rename nightly tar -TAR_NAME=$(ls | grep "roc_nightly.*tar\.gz") -mv "$TAR_NAME" roc_nightly.tar.gz - -# Decompress the tar -tar -xzf roc_nightly.tar.gz - -# Remove the tar file -rm roc_nightly.tar.gz - -# Simplify nightly folder name -NIGHTLY_FOLDER=$(ls -d roc_nightly*/) -mv "$NIGHTLY_FOLDER" roc_nightly - -# Print the roc version -./roc_nightly/roc version - -# Get the latest basic-cli release file URL -CLI_RELEASES_JSON=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/roc-lang/basic-cli/releases) -CLI_RELEASE_URL=$(echo $CLI_RELEASES_JSON | jq -r '.[0].assets | .[] | select(.name | test("\\.tar\\.br$")) | .browser_download_url') - -# Use the latest basic-cli release as the platform for every example -sed -i "s|../platform/main.roc|$CLI_RELEASE_URL|g" $EXAMPLES_DIR/*.roc - -# Install required packages for tests if they're not already available -command -v ncat &>/dev/null || sudo apt install -y ncat -command -v expect &>/dev/null || sudo apt install -y expect - -ROC=./roc_nightly/roc ./ci/all_tests.sh - diff --git a/crates/README.md b/crates/README.md deleted file mode 100644 index 9fba291e..00000000 --- a/crates/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# basic-cli host - -See [basic-cli-build-steps.png](../basic-cli-build-steps.png) to see how these crates -are used in the build process. - -## roc_host - -Every Roc platform needs a host. -The host contains code that calls the Roc main function and provides the Roc app with functions to allocate memory and execute effects such as writing to stdio or making HTTP requests. - -## roc_host_bin - -This crate wraps roc_host to build an executable. This executable is used by `roc preprocess-host ...`. That command generates an .rh and .rm file, these files are used by the [surgical linker](https://github.com/roc-lang/roc/tree/main/crates/linker#the-roc-surgical-linker). - -## roc_host_lib - -This crate wraps roc_host and produces a static library (.a file). This .a file is used for legacy linking. Legacy linking refers to using a typical linker like ld or lld instead of the Roc [surgical linker](https://github.com/roc-lang/roc/tree/main/crates/linker#the-roc-surgical-linker). - -## roc_std - -Provides Rust representations of Roc data structures. \ No newline at end of file diff --git a/crates/roc_command/Cargo.toml b/crates/roc_command/Cargo.toml deleted file mode 100644 index 6963babd..00000000 --- a/crates/roc_command/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "roc_command" -description = "Common functionality for Roc to interface with std::process::Command." - -authors.workspace = true -edition.workspace = true -license.workspace = true -version.workspace = true - -[dependencies] -roc_std.workspace = true -roc_io_error.workspace = true diff --git a/crates/roc_command/src/lib.rs b/crates/roc_command/src/lib.rs deleted file mode 100644 index 00241880..00000000 --- a/crates/roc_command/src/lib.rs +++ /dev/null @@ -1,159 +0,0 @@ -//! This crate provides common functionality for Roc to interface with `std::process::Command` - -use roc_std::{RocList, RocResult, RocStr}; - -#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C)] -pub struct Command { - pub args: RocList, - pub envs: RocList, - pub program: RocStr, - pub clear_envs: bool, -} - -impl roc_std::RocRefcounted for Command { - fn inc(&mut self) { - self.args.inc(); - self.envs.inc(); - self.program.inc(); - } - fn dec(&mut self) { - self.args.dec(); - self.envs.dec(); - self.program.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -impl From<&Command> for std::process::Command { - fn from(roc_cmd: &Command) -> Self { - let args = roc_cmd.args.into_iter().map(|arg| arg.as_str()); - let num_envs = roc_cmd.envs.len() / 2; - let flat_envs = &roc_cmd.envs; - - // Environment variables must be passed in key=value pairs - debug_assert_eq!(flat_envs.len() % 2, 0); - - let mut envs = Vec::with_capacity(num_envs); - for chunk in flat_envs.chunks(2) { - let key = chunk[0].as_str(); - let value = chunk[1].as_str(); - envs.push((key, value)); - } - - let mut cmd = std::process::Command::new(roc_cmd.program.as_str()); - - // Set arguments - cmd.args(args); - - // Clear environment variables - if roc_cmd.clear_envs { - cmd.env_clear(); - }; - - // Set environment variables - cmd.envs(envs); - - cmd - } -} - -#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C)] -pub struct OutputFromHostSuccess { - pub stderr_bytes: roc_std::RocList, - pub stdout_bytes: roc_std::RocList, -} - -#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C)] -pub struct OutputFromHostFailure { - pub stderr_bytes: roc_std::RocList, - pub stdout_bytes: roc_std::RocList, - pub exit_code: i32, -} - -impl roc_std::RocRefcounted for OutputFromHostSuccess { - fn inc(&mut self) { - self.stdout_bytes.inc(); - self.stderr_bytes.inc(); - } - fn dec(&mut self) { - self.stdout_bytes.dec(); - self.stderr_bytes.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -impl roc_std::RocRefcounted for OutputFromHostFailure { - fn inc(&mut self) { - self.exit_code.inc(); - self.stdout_bytes.inc(); - self.stderr_bytes.inc(); - } - fn dec(&mut self) { - self.exit_code.dec(); - self.stdout_bytes.dec(); - self.stderr_bytes.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -pub fn command_exec_exit_code(roc_cmd: &Command) -> RocResult { - match std::process::Command::from(roc_cmd).status() { - Ok(status) => from_exit_status(status), - Err(err) => RocResult::err(err.into()), - } -} - -// Status of the child process, successful/exit code/killed by signal -fn from_exit_status(status: std::process::ExitStatus) -> RocResult { - match status.code() { - Some(code) => RocResult::ok(code), - None => RocResult::err(killed_by_signal_err()), - } -} - -fn killed_by_signal_err() -> roc_io_error::IOErr { - roc_io_error::IOErr { - tag: roc_io_error::IOErrTag::Other, - msg: "Process was killed by operating system signal.".into(), - } -} - -// TODO Can we make this return a tag union (with three variants) ? -pub fn command_exec_output(roc_cmd: &Command) -> RocResult> { - match std::process::Command::from(roc_cmd).output() { - Ok(output) => - match output.status.code() { - Some(status) => { - - let stdout_bytes = RocList::from(&output.stdout[..]); - let stderr_bytes = RocList::from(&output.stderr[..]); - - if status == 0 { - // Success case - RocResult::ok(OutputFromHostSuccess { - stderr_bytes, - stdout_bytes, - }) - } else { - // Failure case - RocResult::err(RocResult::ok(OutputFromHostFailure { - stderr_bytes, - stdout_bytes, - exit_code: status, - })) - } - }, - None => RocResult::err(RocResult::err(killed_by_signal_err())) - } - Err(err) => RocResult::err(RocResult::err(err.into())) - } -} diff --git a/crates/roc_env/Cargo.toml b/crates/roc_env/Cargo.toml deleted file mode 100644 index 81cf59e0..00000000 --- a/crates/roc_env/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "roc_env" -description = "Common functionality for Roc to interface with std::env" - -authors.workspace = true -edition.workspace = true -license.workspace = true -version.workspace = true - -[dependencies] -roc_std.workspace = true -roc_file.workspace = true -sys-locale.workspace = true diff --git a/crates/roc_env/src/arg.rs b/crates/roc_env/src/arg.rs deleted file mode 100644 index bb64b00a..00000000 --- a/crates/roc_env/src/arg.rs +++ /dev/null @@ -1,98 +0,0 @@ -use roc_std::{roc_refcounted_noop_impl, RocList, RocRefcounted}; -use std::ffi::OsString; - -#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C)] -pub struct ArgToAndFromHost { - pub unix: RocList, - pub windows: RocList, - pub tag: ArgTag, -} - -impl From<&[u8]> for ArgToAndFromHost { - #[cfg(target_os = "macos")] - fn from(bytes: &[u8]) -> Self { - ArgToAndFromHost { - unix: RocList::from_slice(bytes), - windows: RocList::empty(), - tag: ArgTag::Unix, - } - } - - #[cfg(target_os = "linux")] - fn from(bytes: &[u8]) -> Self { - ArgToAndFromHost { - unix: RocList::from_slice(bytes), - windows: RocList::empty(), - tag: ArgTag::Unix, - } - } - - #[cfg(target_os = "windows")] - fn from(bytes: &[u8]) -> Self { - todo!() - // use something like - // https://docs.rs/widestring/latest/widestring/ - // to support Windows - } -} - -impl From for ArgToAndFromHost { - #[cfg(target_os = "macos")] - fn from(os_str: OsString) -> Self { - ArgToAndFromHost { - unix: RocList::from_slice(os_str.as_encoded_bytes()), - windows: RocList::empty(), - tag: ArgTag::Unix, - } - } - - #[cfg(target_os = "linux")] - fn from(os_str: OsString) -> Self { - ArgToAndFromHost { - unix: RocList::from_slice(os_str.as_encoded_bytes()), - windows: RocList::empty(), - tag: ArgTag::Unix, - } - } - - #[cfg(target_os = "windows")] - fn from(os_str: OsString) -> Self { - todo!() - // use something like - // https://docs.rs/widestring/latest/widestring/ - // to support Windows - } -} - -#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(u8)] -pub enum ArgTag { - Unix = 0, - Windows = 1, -} - -impl core::fmt::Debug for ArgTag { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Unix => f.write_str("ArgTag::Unix"), - Self::Windows => f.write_str("ArgTag::Windows"), - } - } -} - -roc_refcounted_noop_impl!(ArgTag); - -impl roc_std::RocRefcounted for ArgToAndFromHost { - fn inc(&mut self) { - self.unix.inc(); - self.windows.inc(); - } - fn dec(&mut self) { - self.unix.dec(); - self.windows.dec(); - } - fn is_refcounted() -> bool { - true - } -} diff --git a/crates/roc_env/src/lib.rs b/crates/roc_env/src/lib.rs deleted file mode 100644 index ca8e591a..00000000 --- a/crates/roc_env/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! This crate provides common functionality common functionality for Roc to interface with `std::env` -pub mod arg; - -use roc_std::{roc_refcounted_noop_impl, RocList, RocRefcounted, RocResult, RocStr}; -use std::borrow::Borrow; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -pub fn env_dict() -> RocList<(RocStr, RocStr)> { - // TODO: can we be more efficient about reusing the String's memory for RocStr? - std::env::vars_os() - .map(|(key, val)| { - ( - RocStr::from(key.to_string_lossy().borrow()), - RocStr::from(val.to_string_lossy().borrow()), - ) - }) - .collect() -} - -pub fn temp_dir() -> RocList { - let path_os_string_bytes = std::env::temp_dir().into_os_string().into_encoded_bytes(); - - RocList::from(path_os_string_bytes.as_slice()) -} - -pub fn env_var(roc_str: &RocStr) -> RocResult { - // TODO: can we be more efficient about reusing the String's memory for RocStr? - match std::env::var_os(roc_str.as_str()) { - Some(os_str) => RocResult::ok(RocStr::from(os_str.to_string_lossy().borrow())), - None => RocResult::err(()), - } -} - -pub fn cwd() -> RocResult, ()> { - // TODO instead, call getcwd on UNIX and GetCurrentDirectory on Windows - match std::env::current_dir() { - Ok(path_buf) => RocResult::ok(roc_file::os_str_to_roc_path( - path_buf.into_os_string().as_os_str(), - )), - Err(_) => { - // Default to empty path - RocResult::ok(RocList::empty()) - } - } -} - -pub fn set_cwd(roc_path: &RocList) -> RocResult<(), ()> { - match std::env::set_current_dir(roc_file::path_from_roc_path(roc_path)) { - Ok(()) => RocResult::ok(()), - Err(_) => RocResult::err(()), - } -} - -pub fn exe_path() -> RocResult, ()> { - match std::env::current_exe() { - Ok(path_buf) => RocResult::ok(roc_file::os_str_to_roc_path(path_buf.as_path().as_os_str())), - Err(_) => RocResult::err(()), - } -} - -pub fn get_locale() -> RocResult { - sys_locale::get_locale().map_or_else( - || RocResult::err(()), - |locale| RocResult::ok(locale.to_string().as_str().into()), - ) -} - -pub fn get_locales() -> RocList { - const DEFAULT_MAX_LOCALES: usize = 10; - let locales = sys_locale::get_locales(); - let mut roc_locales = RocList::with_capacity(DEFAULT_MAX_LOCALES); - for l in locales { - roc_locales.push(l.to_string().as_str().into()); - } - roc_locales -} - -#[derive(Debug)] -#[repr(C)] -pub struct ReturnArchOS { - pub arch: RocStr, - pub os: RocStr, -} - -roc_refcounted_noop_impl!(ReturnArchOS); - -pub fn current_arch_os() -> ReturnArchOS { - ReturnArchOS { - arch: std::env::consts::ARCH.into(), - os: std::env::consts::OS.into(), - } -} - -pub fn posix_time() -> roc_std::U128 { - // TODO in future may be able to avoid this panic by using C APIs - let since_epoch = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("time went backwards"); - - roc_std::U128::from(since_epoch.as_nanos()) -} - -pub fn sleep_millis(milliseconds: u64) { - let duration = Duration::from_millis(milliseconds); - std::thread::sleep(duration); -} diff --git a/crates/roc_file/Cargo.toml b/crates/roc_file/Cargo.toml deleted file mode 100644 index caf47b11..00000000 --- a/crates/roc_file/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "roc_file" -description = "Common functionality for Roc to interface with std::io" - -authors.workspace = true -edition.workspace = true -license.workspace = true -version.workspace = true - -[dependencies] -roc_std.workspace = true -roc_std_heap.workspace = true -roc_io_error.workspace = true -memchr.workspace = true diff --git a/crates/roc_file/src/lib.rs b/crates/roc_file/src/lib.rs deleted file mode 100644 index 12dd2f5e..00000000 --- a/crates/roc_file/src/lib.rs +++ /dev/null @@ -1,439 +0,0 @@ -//! This crate provides common functionality for Roc to interface with `std::io` -use roc_io_error::{IOErr, IOErrTag}; -use roc_std::{RocBox, RocList, RocResult, RocStr}; -use roc_std_heap::ThreadSafeRefcountedResourceHeap; -use std::borrow::Cow; -use std::ffi::OsStr; -use std::fs::File; -use std::io::{BufRead, BufReader, ErrorKind, Read, Write}; -use std::path::Path; -use std::sync::OnceLock; -use std::{env, io}; - -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; // used for is_executable, is_readable, is_writable - -pub fn heap() -> &'static ThreadSafeRefcountedResourceHeap> { - static FILE_HEAP: OnceLock>> = OnceLock::new(); - FILE_HEAP.get_or_init(|| { - let default_max_files = 65536; - let max_files = env::var("ROC_BASIC_CLI_MAX_FILES") - .map(|v| v.parse().unwrap_or(default_max_files)) - .unwrap_or(default_max_files); - ThreadSafeRefcountedResourceHeap::new(max_files) - .expect("Failed to allocate mmap for file handle references.") - }) -} - -pub fn file_write_utf8(roc_path: &RocList, roc_str: &RocStr) -> RocResult<(), IOErr> { - write_slice(roc_path, roc_str.as_str().as_bytes()) -} - -pub fn file_write_bytes(roc_path: &RocList, roc_bytes: &RocList) -> RocResult<(), IOErr> { - write_slice(roc_path, roc_bytes.as_slice()) -} - -fn write_slice(roc_path: &RocList, bytes: &[u8]) -> RocResult<(), IOErr> { - match File::create(path_from_roc_path(roc_path)) { - Ok(mut file) => match file.write_all(bytes) { - Ok(()) => RocResult::ok(()), - Err(err) => RocResult::err(err.into()), - }, - Err(err) => RocResult::err(err.into()), - } -} - -#[repr(C)] -pub struct InternalPathType { - is_dir: bool, - is_file: bool, - is_sym_link: bool, -} - -pub fn path_type(roc_path: &RocList) -> RocResult { - let path = path_from_roc_path(roc_path); - match path.symlink_metadata() { - Ok(m) => RocResult::ok(InternalPathType { - is_dir: m.is_dir(), - is_file: m.is_file(), - is_sym_link: m.is_symlink(), - }), - Err(err) => RocResult::err(err.into()), - } -} - -#[cfg(target_family = "unix")] -pub fn path_from_roc_path(bytes: &RocList) -> Cow<'_, Path> { - use std::os::unix::ffi::OsStrExt; - let os_str = OsStr::from_bytes(bytes.as_slice()); - Cow::Borrowed(Path::new(os_str)) -} - -#[cfg(target_family = "windows")] -pub fn path_from_roc_path(bytes: &RocList) -> Cow<'_, Path> { - use std::os::windows::ffi::OsStringExt; - - let bytes = bytes.as_slice(); - assert_eq!(bytes.len() % 2, 0); - let characters: &[u16] = - unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast(), bytes.len() / 2) }; - - let os_string = std::ffi::OsString::from_wide(characters); - - Cow::Owned(std::path::PathBuf::from(os_string)) -} - -pub fn file_read_bytes(roc_path: &RocList) -> RocResult, IOErr> { - // TODO: write our own duplicate of `read_to_end` that directly fills a `RocList`. - // This adds an extra O(n) copy. - let mut bytes = Vec::new(); - - match File::open(path_from_roc_path(roc_path)) { - Ok(mut file) => match file.read_to_end(&mut bytes) { - Ok(_bytes_read) => RocResult::ok(RocList::from(bytes.as_slice())), - Err(err) => RocResult::err(err.into()), - }, - Err(err) => RocResult::err(err.into()), - } -} - -pub fn file_reader(roc_path: &RocList, size: u64) -> RocResult, IOErr> { - match File::open(path_from_roc_path(roc_path)) { - Ok(file) => { - let buf_reader = if size > 0 { - BufReader::with_capacity(size as usize, file) - } else { - BufReader::new(file) - }; - - let heap = heap(); - let alloc_result = heap.alloc_for(buf_reader); - match alloc_result { - Ok(out) => RocResult::ok(out), - Err(err) => RocResult::err(err.into()), - } - } - Err(err) => RocResult::err(err.into()), - } -} - -pub fn file_read_line(data: RocBox<()>) -> RocResult, IOErr> { - let buf_reader: &mut BufReader = ThreadSafeRefcountedResourceHeap::box_to_resource(data); - - let mut buffer = RocList::empty(); - match read_until(buf_reader, b'\n', &mut buffer) { - Ok(..) => { - // Note: this returns an empty list when no bytes were read, e.g. End Of File - RocResult::ok(buffer) - } - Err(err) => RocResult::err(err.into()), - } -} - -pub fn read_until( - r: &mut R, - delim: u8, - buf: &mut RocList, -) -> io::Result { - let mut read = 0; - loop { - let (done, used) = { - let available = match r.fill_buf() { - Ok(n) => n, - Err(ref e) if matches!(e.kind(), ErrorKind::Interrupted) => continue, - Err(e) => return Err(e), - }; - match memchr::memchr(delim, available) { - Some(i) => { - buf.extend_from_slice(&available[..=i]); - (true, i + 1) - } - None => { - buf.extend_from_slice(available); - (false, available.len()) - } - } - }; - r.consume(used); - read += used; - if done || used == 0 { - return Ok(read); - } - } -} - -pub fn file_delete(roc_path: &RocList) -> RocResult<(), IOErr> { - match std::fs::remove_file(path_from_roc_path(roc_path)) { - Ok(()) => RocResult::ok(()), - Err(err) => RocResult::err(err.into()), - } -} - -/// Note: If the path is a directory or symlink, you probably don't want to call this function. -pub fn file_size_in_bytes(roc_path: &RocList) -> RocResult { - let rust_path = path_from_roc_path(roc_path); - let metadata_res = std::fs::metadata(rust_path); - - match metadata_res { - Ok(metadata) => { - RocResult::ok(metadata.len()) - } - Err(err) => { - RocResult::err(err.into()) - } - } -} - -pub fn file_is_executable(roc_path: &RocList) -> RocResult { - let rust_path = path_from_roc_path(roc_path); - - #[cfg(unix)] - { - let metadata_res = std::fs::metadata(rust_path); - - match metadata_res { - Ok(metadata) => { - let permissions = metadata.permissions(); - RocResult::ok(permissions.mode() & 0o111 != 0) - } - Err(err) => { - RocResult::err(err.into()) - } - } - } - - #[cfg(windows)] - { - RocResult::err(IOErr{ - msg: "Not yet implemented on windows.".into(), - tag: IOErrTag::Unsupported, - }) - } -} - -pub fn file_is_readable(roc_path: &RocList) -> RocResult { - let rust_path = path_from_roc_path(roc_path); - - #[cfg(unix)] - { - let metadata_res = std::fs::metadata(rust_path); - - match metadata_res { - Ok(metadata) => { - let permissions = metadata.permissions(); - RocResult::ok(permissions.mode() & 0o400 != 0) - } - Err(err) => { - RocResult::err(err.into()) - } - } - } - - #[cfg(windows)] - { - RocResult::err(IOErr{ - msg: "Not yet implemented on windows.".into(), - tag: IOErrTag::Unsupported, - }) - } -} - -pub fn file_is_writable(roc_path: &RocList) -> RocResult { - let rust_path = path_from_roc_path(roc_path); - - #[cfg(unix)] - { - let metadata_res = std::fs::metadata(rust_path); - - match metadata_res { - Ok(metadata) => { - let permissions = metadata.permissions(); - RocResult::ok(permissions.mode() & 0o200 != 0) - } - Err(err) => { - RocResult::err(err.into()) - } - } - } - - #[cfg(windows)] - { - RocResult::err(IOErr{ - msg: "Not yet implemented on windows.".into(), - tag: IOErrTag::Unsupported, - }) - } -} - -pub fn file_time_accessed(roc_path: &RocList) -> RocResult { - let rust_path = path_from_roc_path(roc_path); - let metadata_res = std::fs::metadata(rust_path); - - match metadata_res { - Ok(metadata) => { - let accessed = metadata.accessed(); - match accessed { - Ok(time) => { - RocResult::ok( - roc_std::U128::from( - time.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() - ) - ) - } - Err(err) => { - RocResult::err(err.into()) - } - } - } - Err(err) => { - RocResult::err(err.into()) - } - } -} - -pub fn file_time_modified(roc_path: &RocList) -> RocResult { - let rust_path = path_from_roc_path(roc_path); - let metadata_res = std::fs::metadata(rust_path); - - match metadata_res { - Ok(metadata) => { - let modified = metadata.modified(); - match modified { - Ok(time) => { - RocResult::ok( - roc_std::U128::from( - time.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() - ) - ) - } - Err(err) => { - RocResult::err(err.into()) - } - } - } - Err(err) => { - RocResult::err(err.into()) - } - } -} - -pub fn file_time_created(roc_path: &RocList) -> RocResult { - let rust_path = path_from_roc_path(roc_path); - let metadata_res = std::fs::metadata(rust_path); - - match metadata_res { - Ok(metadata) => { - let created = metadata.created(); - match created { - Ok(time) => { - RocResult::ok( - roc_std::U128::from( - time.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() - ) - ) - } - Err(err) => { - RocResult::err(err.into()) - } - } - } - Err(err) => { - RocResult::err(err.into()) - } - } -} - -pub fn file_exists(roc_path: &RocList) -> RocResult { - let path = path_from_roc_path(roc_path); - match path.try_exists() { - Ok(exists) => RocResult::ok(exists), - Err(err) => RocResult::err(err.into()), - } -} - -pub fn file_rename(from_path: &RocList, to_path: &RocList) -> RocResult<(), IOErr> { - let rust_from_path = path_from_roc_path(from_path); - let rust_to_path = path_from_roc_path(to_path); - - match std::fs::rename(rust_from_path, rust_to_path) { - Ok(()) => RocResult::ok(()), - Err(err) => RocResult::err(err.into()), - } -} - -pub fn dir_list(roc_path: &RocList) -> RocResult>, IOErr> { - let path = path_from_roc_path(roc_path); - - if path.is_dir() { - let dir = match std::fs::read_dir(path) { - Ok(dir) => dir, - Err(err) => return RocResult::err(err.into()), - }; - - let mut entries = Vec::new(); - - for entry in dir.flatten() { - let path = entry.path(); - let str = path.as_os_str(); - entries.push(os_str_to_roc_path(str)); - } - - RocResult::ok(RocList::from_iter(entries)) - } else { - RocResult::err(IOErr { - msg: "NotADirectory".into(), - tag: IOErrTag::Other, - }) - } -} - -pub fn dir_create(roc_path: &RocList) -> RocResult<(), IOErr> { - match std::fs::create_dir(path_from_roc_path(roc_path)) { - Ok(_) => RocResult::ok(()), - Err(err) => RocResult::err(err.into()), - } -} - -pub fn dir_create_all(roc_path: &RocList) -> RocResult<(), IOErr> { - match std::fs::create_dir_all(path_from_roc_path(roc_path)) { - Ok(_) => RocResult::ok(()), - Err(err) => RocResult::err(err.into()), - } -} - -pub fn dir_delete_empty(roc_path: &RocList) -> RocResult<(), IOErr> { - match std::fs::remove_dir(path_from_roc_path(roc_path)) { - Ok(_) => RocResult::ok(()), - Err(err) => RocResult::err(err.into()), - } -} - -pub fn dir_delete_all(roc_path: &RocList) -> RocResult<(), IOErr> { - match std::fs::remove_dir_all(path_from_roc_path(roc_path)) { - Ok(_) => RocResult::ok(()), - Err(err) => RocResult::err(err.into()), - } -} - -pub fn hard_link(path_from: &RocList, path_to: &RocList) -> RocResult<(), IOErr> { - match std::fs::hard_link(path_from_roc_path(path_from), path_from_roc_path(path_to)) { - Ok(_) => RocResult::ok(()), - Err(err) => RocResult::err(err.into()), - } -} - -#[cfg(target_family = "unix")] -pub fn os_str_to_roc_path(os_str: &OsStr) -> RocList { - use std::os::unix::ffi::OsStrExt; - - RocList::from(os_str.as_bytes()) -} - -#[cfg(target_family = "windows")] -pub fn os_str_to_roc_path(os_str: &OsStr) -> RocList { - use std::os::windows::ffi::OsStrExt; - - let bytes: Vec<_> = os_str.encode_wide().flat_map(|c| c.to_be_bytes()).collect(); - - RocList::from(bytes.as_slice()) -} diff --git a/crates/roc_host/Cargo.toml b/crates/roc_host/Cargo.toml deleted file mode 100644 index 882b0178..00000000 --- a/crates/roc_host/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "roc_host" -version = "0.0.1" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -description = "This provides the [host](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#host) implementation for the platform." - -links = "app" - -[lib] -name = "roc_host" -path = "src/lib.rs" - -[dependencies] -crossterm.workspace = true -memmap2.workspace = true -memchr.workspace = true -sys-locale.workspace = true -libc.workspace = true -backtrace.workspace = true -roc_std.workspace = true -roc_std_heap.workspace = true -roc_command.workspace = true -roc_file.workspace = true -roc_io_error.workspace = true -roc_http.workspace = true -roc_stdio.workspace = true -roc_env.workspace = true -roc_random.workspace = true -roc_sqlite.workspace = true -hyper.workspace = true -hyper-rustls.workspace = true -tokio.workspace = true -bytes.workspace = true -http-body-util.workspace = true -hyper-util.workspace = true diff --git a/crates/roc_host/build.rs b/crates/roc_host/build.rs deleted file mode 100644 index b124ebd5..00000000 --- a/crates/roc_host/build.rs +++ /dev/null @@ -1,27 +0,0 @@ -/// Link the stubbed app shared library file with the roc_host_bin executable -fn main() { - // The path to the platform directory within the workspace - // where the libapp.so file is generated by the build.roc script - let platform_path = workspace_dir().join("platform"); - - println!("cargo:rustc-link-search={}", platform_path.display()); - - #[cfg(not(windows))] - println!("cargo:rustc-link-lib=dylib=app"); - - #[cfg(windows)] - println!("cargo:rustc-link-lib=dylib=libapp"); -} - -/// Gets the path to the workspace root. -fn workspace_dir() -> std::path::PathBuf { - let output = std::process::Command::new(env!("CARGO")) - .arg("locate-project") - .arg("--workspace") - .arg("--message-format=plain") - .output() - .unwrap() - .stdout; - let cargo_path = std::path::Path::new(std::str::from_utf8(&output).unwrap().trim()); - cargo_path.parent().unwrap().to_path_buf() -} diff --git a/crates/roc_host/src/lib.rs b/crates/roc_host/src/lib.rs deleted file mode 100644 index b9fe3b69..00000000 --- a/crates/roc_host/src/lib.rs +++ /dev/null @@ -1,864 +0,0 @@ -//! Implementation of the host. -//! The host contains code that calls the Roc main function and provides the -//! Roc app with functions to allocate memory and execute effects such as -//! writing to stdio or making HTTP requests. - -use core::ffi::c_void; -use bytes::Bytes; -use http_body_util::BodyExt; -use hyper_util::rt::TokioExecutor; -use roc_env::arg::ArgToAndFromHost; -use roc_io_error::IOErr; -use roc_std::{RocBox, RocList, RocResult, RocStr}; -use std::time::Duration; -use tokio::runtime::Runtime; - -thread_local! { - static TOKIO_RUNTIME: Runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); -} - -/// # Safety -/// -/// This function is unsafe. -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - libc::malloc(size) -} - -/// # Safety -/// -/// This function is unsafe. -#[no_mangle] -pub unsafe extern "C" fn roc_realloc( - c_ptr: *mut c_void, - new_size: usize, - _old_size: usize, - _alignment: u32, -) -> *mut c_void { - libc::realloc(c_ptr, new_size) -} - -/// # Safety -/// -/// This function is unsafe. -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - let heap = roc_file::heap(); - if heap.in_range(c_ptr) { - heap.dealloc(c_ptr); - return; - } - let heap = roc_http::heap(); - if heap.in_range(c_ptr) { - heap.dealloc(c_ptr); - return; - } - // !! If you make any changes to this function, you may also need to update roc_dealloc in - // https://github.com/roc-lang/basic-webserver - let heap = roc_sqlite::heap(); - if heap.in_range(c_ptr) { - heap.dealloc(c_ptr); - return; - } - libc::free(c_ptr) -} - -/// # Safety -/// -/// This function is unsafe. -#[no_mangle] -pub unsafe extern "C" fn roc_panic(msg: &RocStr, tag_id: u32) { - _ = crossterm::terminal::disable_raw_mode(); - match tag_id { - 0 => { - eprintln!("Roc crashed with:\n\n\t{}\n", msg.as_str()); - - print_backtrace(); - std::process::exit(1); - } - 1 => { - eprintln!("The program crashed with:\n\n\t{}\n", msg.as_str()); - - print_backtrace(); - std::process::exit(1); - } - _ => todo!(), - } -} - -/// # Safety -/// -/// This function is unsafe. -#[no_mangle] -pub unsafe extern "C" fn roc_dbg(loc: &RocStr, msg: &RocStr, src: &RocStr) { - eprintln!("[{}] {} = {}", loc, src, msg); -} - -#[repr(C)] -pub struct Variable { - pub name: RocStr, - pub value: RocStr, -} - -impl roc_std::RocRefcounted for Variable { - fn inc(&mut self) { - self.name.inc(); - self.value.inc(); - } - fn dec(&mut self) { - self.name.dec(); - self.value.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -/// This is not currently used but has been included for a future upgrade to roc -/// to help with debugging and prevent a breaking change for users -/// refer to -/// -/// # Safety -/// -/// This function is unsafe. -#[no_mangle] -pub unsafe extern "C" fn roc_expect_failed( - loc: &RocStr, - src: &RocStr, - variables: &RocList, -) { - eprintln!("\nExpectation failed at {}:", loc.as_str()); - eprintln!("\nExpression:\n\t{}\n", src.as_str()); - - if !variables.is_empty() { - eprintln!("With values:"); - for var in variables.iter() { - eprintln!("\t{} = {}", var.name.as_str(), var.value.as_str()); - } - eprintln!(); - } - - std::process::exit(1); -} - -/// # Safety -/// -/// This function is unsafe. -#[cfg(unix)] -#[no_mangle] -pub unsafe extern "C" fn roc_getppid() -> libc::pid_t { - libc::getppid() -} - -/// # Safety -/// -/// This function should be called with a valid addr pointer. -#[cfg(unix)] -#[no_mangle] -pub unsafe extern "C" fn roc_mmap( - addr: *mut libc::c_void, - len: libc::size_t, - prot: libc::c_int, - flags: libc::c_int, - fd: libc::c_int, - offset: libc::off_t, -) -> *mut libc::c_void { - libc::mmap(addr, len, prot, flags, fd, offset) -} - -/// # Safety -/// -/// This function should be called with a valid name pointer. -#[cfg(unix)] -#[no_mangle] -pub unsafe extern "C" fn roc_shm_open( - name: *const libc::c_char, - oflag: libc::c_int, - mode: libc::mode_t, -) -> libc::c_int { - libc::shm_open(name, oflag, mode as libc::c_uint) -} - -fn print_backtrace() { - eprintln!("Here is the call stack that led to the crash:\n"); - - let mut entries = Vec::new(); - - #[derive(Default)] - struct Entry { - pub fn_name: String, - pub filename: Option, - pub line: Option, - pub col: Option, - } - - backtrace::trace(|frame| { - backtrace::resolve_frame(frame, |symbol| { - if let Some(fn_name) = symbol.name() { - let fn_name = fn_name.to_string(); - - if should_show_in_backtrace(&fn_name) { - let mut entry = Entry { - fn_name: format_fn_name(&fn_name), - ..Default::default() - }; - - if let Some(path) = symbol.filename() { - entry.filename = Some(path.to_string_lossy().into_owned()); - }; - - entry.line = symbol.lineno(); - entry.col = symbol.colno(); - - entries.push(entry); - } - } else { - entries.push(Entry { - fn_name: "???".to_string(), - ..Default::default() - }); - } - }); - - true // keep going to the next frame - }); - - for entry in entries { - eprintln!("\t{}", entry.fn_name); - - if let Some(filename) = entry.filename { - eprintln!("\t\t{filename}"); - } - } - - eprintln!("\nOptimizations can make this list inaccurate! If it looks wrong, try running without `--optimize` and with `--linker=legacy`\n"); -} - -fn should_show_in_backtrace(fn_name: &str) -> bool { - let is_from_rust = fn_name.contains("::"); - let is_host_fn = fn_name.starts_with("roc_panic") - || fn_name.starts_with("_roc__") - || fn_name.starts_with("rust_main") - || fn_name == "_main"; - - !is_from_rust && !is_host_fn -} - -fn format_fn_name(fn_name: &str) -> String { - // e.g. convert "_Num_sub_a0c29024d3ec6e3a16e414af99885fbb44fa6182331a70ab4ca0886f93bad5" - // to ["Num", "sub", "a0c29024d3ec6e3a16e414af99885fbb44fa6182331a70ab4ca0886f93bad5"] - let mut pieces_iter = fn_name.split('_'); - - if let (_, Some(module_name), Some(name)) = - (pieces_iter.next(), pieces_iter.next(), pieces_iter.next()) - { - display_roc_fn(module_name, name) - } else { - "???".to_string() - } -} - -fn display_roc_fn(module_name: &str, fn_name: &str) -> String { - let module_name = if module_name == "#UserApp" { - "app" - } else { - module_name - }; - - let fn_name = if fn_name.parse::().is_ok() { - "(anonymous function)" - } else { - fn_name - }; - - format!("\u{001B}[36m{module_name}\u{001B}[39m.{fn_name}") -} - -/// # Safety -/// -/// This function should be provided a valid dst pointer. -#[no_mangle] -pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void { - libc::memset(dst, c, n) -} - -// Protect our functions from the vicious GC. -// This is specifically a problem with static compilation and musl. -// TODO: remove all of this when we switch to effect interpreter. -pub fn init() { - let funcs: &[*const extern "C" fn()] = &[ - roc_alloc as _, - roc_realloc as _, - roc_dealloc as _, - roc_panic as _, - roc_dbg as _, - roc_memset as _, - roc_fx_env_dict as _, - roc_fx_env_var as _, - roc_fx_set_cwd as _, - roc_fx_exe_path as _, - roc_fx_stdin_line as _, - roc_fx_stdin_bytes as _, - roc_fx_stdin_read_to_end as _, - roc_fx_stdout_line as _, - roc_fx_stdout_write as _, - roc_fx_stdout_write_bytes as _, - roc_fx_stderr_line as _, - roc_fx_stderr_write as _, - roc_fx_stderr_write_bytes as _, - roc_fx_tty_mode_canonical as _, - roc_fx_tty_mode_raw as _, - roc_fx_file_write_utf8 as _, - roc_fx_file_write_bytes as _, - roc_fx_path_type as _, - roc_fx_file_read_bytes as _, - roc_fx_file_reader as _, - roc_fx_file_read_line as _, - roc_fx_file_delete as _, - roc_fx_file_size_in_bytes as _, - roc_fx_file_is_executable as _, - roc_fx_file_is_readable as _, - roc_fx_file_is_writable as _, - roc_fx_file_time_accessed as _, - roc_fx_file_time_modified as _, - roc_fx_file_time_created as _, - roc_fx_file_exists as _, - roc_fx_file_rename as _, - roc_fx_hard_link as _, - roc_fx_cwd as _, - roc_fx_posix_time as _, - roc_fx_sleep_millis as _, - roc_fx_dir_list as _, - roc_fx_send_request as _, - roc_fx_tcp_connect as _, - roc_fx_tcp_read_up_to as _, - roc_fx_tcp_read_exactly as _, - roc_fx_tcp_read_until as _, - roc_fx_tcp_write as _, - roc_fx_command_exec_exit_code as _, - roc_fx_command_exec_output as _, - roc_fx_dir_create as _, - roc_fx_dir_create_all as _, - roc_fx_dir_delete_empty as _, - roc_fx_dir_delete_all as _, - roc_fx_current_arch_os as _, - roc_fx_temp_dir as _, - roc_fx_get_locale as _, - roc_fx_get_locales as _, - roc_fx_random_u64 as _, - roc_fx_random_u32 as _, - roc_fx_sqlite_bind as _, - roc_fx_sqlite_column_value as _, - roc_fx_sqlite_columns as _, - roc_fx_sqlite_prepare as _, - roc_fx_sqlite_reset as _, - roc_fx_sqlite_step as _, - ]; - #[allow(forgetting_references)] - std::mem::forget(std::hint::black_box(funcs)); - if cfg!(unix) { - let unix_funcs: &[*const extern "C" fn()] = - &[roc_getppid as _, roc_mmap as _, roc_shm_open as _]; - #[allow(forgetting_references)] - std::mem::forget(std::hint::black_box(unix_funcs)); - } -} - -#[no_mangle] -pub extern "C" fn rust_main(args: RocList) -> i32 { - init(); - - extern "C" { - #[link_name = "roc__main_for_host_1_exposed_generic"] - pub fn roc_main_for_host_caller( - exit_code: &mut i32, - args: *const RocList, - ); - - #[link_name = "roc__main_for_host_1_exposed_size"] - pub fn roc_main__for_host_size() -> usize; - } - - let exit_code: i32 = unsafe { - let mut exit_code: i32 = -1; - let args = args; - roc_main_for_host_caller(&mut exit_code, &args); - - debug_assert_eq!(std::mem::size_of_val(&exit_code), roc_main__for_host_size()); - - // roc now owns the args so prevent the args from being - // dropped by rust and causing a double free - std::mem::forget(args); - - exit_code - }; - - exit_code -} - -#[no_mangle] -pub extern "C" fn roc_fx_env_dict() -> RocList<(RocStr, RocStr)> { - roc_env::env_dict() -} - -#[no_mangle] -pub extern "C" fn roc_fx_env_var(roc_str: &RocStr) -> RocResult { - roc_env::env_var(roc_str) -} - -#[no_mangle] -pub extern "C" fn roc_fx_set_cwd(roc_path: &RocList) -> RocResult<(), ()> { - roc_env::set_cwd(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_exe_path() -> RocResult, ()> { - roc_env::exe_path() -} - -#[no_mangle] -pub extern "C" fn roc_fx_stdin_line() -> RocResult { - roc_stdio::stdin_line() -} - -#[no_mangle] -pub extern "C" fn roc_fx_stdin_bytes() -> RocResult, roc_io_error::IOErr> { - roc_stdio::stdin_bytes() -} - -#[no_mangle] -pub extern "C" fn roc_fx_stdin_read_to_end() -> RocResult, roc_io_error::IOErr> { - roc_stdio::stdin_read_to_end() -} - -#[no_mangle] -pub extern "C" fn roc_fx_stdout_line(line: &RocStr) -> RocResult<(), roc_io_error::IOErr> { - roc_stdio::stdout_line(line) -} - -#[no_mangle] -pub extern "C" fn roc_fx_stdout_write(text: &RocStr) -> RocResult<(), roc_io_error::IOErr> { - roc_stdio::stdout_write(text) -} - -#[no_mangle] -pub extern "C" fn roc_fx_stdout_write_bytes(bytes: &RocList) -> RocResult<(), roc_io_error::IOErr> { - roc_stdio::stdout_write_bytes(bytes) -} - -#[no_mangle] -pub extern "C" fn roc_fx_stderr_line(line: &RocStr) -> RocResult<(), roc_io_error::IOErr> { - roc_stdio::stderr_line(line) -} - -#[no_mangle] -pub extern "C" fn roc_fx_stderr_write(text: &RocStr) -> RocResult<(), roc_io_error::IOErr> { - roc_stdio::stderr_write(text) -} - -#[no_mangle] -pub extern "C" fn roc_fx_stderr_write_bytes(bytes: &RocList) -> RocResult<(), roc_io_error::IOErr> { - roc_stdio::stderr_write_bytes(bytes) -} - -#[no_mangle] -pub extern "C" fn roc_fx_tty_mode_canonical() { - crossterm::terminal::disable_raw_mode().expect("failed to disable raw mode"); -} - -#[no_mangle] -pub extern "C" fn roc_fx_tty_mode_raw() { - crossterm::terminal::enable_raw_mode().expect("failed to enable raw mode"); -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_write_utf8( - roc_path: &RocList, - roc_str: &RocStr, -) -> RocResult<(), IOErr> { - roc_file::file_write_utf8(roc_path, roc_str) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_write_bytes( - roc_path: &RocList, - roc_bytes: &RocList, -) -> RocResult<(), roc_io_error::IOErr> { - roc_file::file_write_bytes(roc_path, roc_bytes) -} - -#[no_mangle] -pub extern "C" fn roc_fx_path_type( - roc_path: &RocList, -) -> RocResult { - roc_file::path_type(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_read_bytes( - roc_path: &RocList, -) -> RocResult, roc_io_error::IOErr> { - roc_file::file_read_bytes(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_reader( - roc_path: &RocList, - size: u64, -) -> RocResult, roc_io_error::IOErr> { - roc_file::file_reader(roc_path, size) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_read_line( - data: RocBox<()>, -) -> RocResult, roc_io_error::IOErr> { - roc_file::file_read_line(data) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_delete(roc_path: &RocList) -> RocResult<(), roc_io_error::IOErr> { - roc_file::file_delete(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_size_in_bytes( - roc_path: &RocList, -) -> RocResult { - roc_file::file_size_in_bytes(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_is_executable( - roc_path: &RocList, -) -> RocResult { - roc_file::file_is_executable(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_is_readable( - roc_path: &RocList, -) -> RocResult { - roc_file::file_is_readable(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_is_writable( - roc_path: &RocList, -) -> RocResult { - roc_file::file_is_writable(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_time_accessed( - roc_path: &RocList, -) -> RocResult { - roc_file::file_time_accessed(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_time_modified( - roc_path: &RocList, -) -> RocResult { - roc_file::file_time_modified(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_time_created( - roc_path: &RocList, -) -> RocResult { - roc_file::file_time_created(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_exists(roc_path: &RocList) -> RocResult { - roc_file::file_exists(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_file_rename( - from_path: &RocList, - to_path: &RocList, -) -> RocResult<(), roc_io_error::IOErr> { - roc_file::file_rename(from_path, to_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_cwd() -> RocResult, ()> { - roc_env::cwd() -} - -#[no_mangle] -pub extern "C" fn roc_fx_posix_time() -> roc_std::U128 { - roc_env::posix_time() -} - -#[no_mangle] -pub extern "C" fn roc_fx_sleep_millis(milliseconds: u64) { - roc_env::sleep_millis(milliseconds); -} - -#[no_mangle] -pub extern "C" fn roc_fx_dir_list( - roc_path: &RocList, -) -> RocResult>, roc_io_error::IOErr> { - roc_file::dir_list(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_send_request( - roc_request: &roc_http::RequestToAndFromHost, -) -> roc_http::ResponseToAndFromHost { - TOKIO_RUNTIME.with(|rt| { - let request = match roc_request.to_hyper_request() { - Ok(r) => r, - Err(err) => return err.into(), - }; - - match roc_request.has_timeout() { - Some(time_limit) => rt - .block_on(async { - tokio::time::timeout( - Duration::from_millis(time_limit), - async_send_request(request), - ) - .await - }) - .unwrap_or_else(|_err| roc_http::ResponseToAndFromHost { - status: 408, - headers: RocList::empty(), - body: roc_http::REQUEST_TIMEOUT_BODY.into(), - }), - None => rt.block_on(async_send_request(request)), - } - }) -} - -async fn async_send_request(request: hyper::Request>) -> roc_http::ResponseToAndFromHost { - use hyper_util::client::legacy::Client; - use hyper_rustls::HttpsConnectorBuilder; - - let https = match HttpsConnectorBuilder::new() - .with_native_roots() - { - Ok(builder) => builder - .https_or_http() - .enable_http1() - .build(), - Err(_) => { - return roc_http::ResponseToAndFromHost { - status: 500, - headers: RocList::empty(), - body: "Failed to initialize HTTPS connector with native roots".as_bytes().into(), - }; - } - }; - - let client: Client<_, http_body_util::Full> = - Client::builder(TokioExecutor::new()).build(https); - - let response_res = client.request(request).await; - - match response_res { - Ok(response) => { - let status = response.status(); - - let headers = RocList::from_iter(response.headers().iter().map(|(name, value)| { - roc_http::Header::new(name.as_str(), value.to_str().unwrap_or_default()) - })); - - let status = status.as_u16(); - - let bytes_res = - response.into_body().collect().await.map(|collected| collected.to_bytes()); - - match bytes_res { - Ok(bytes) => { - let body: RocList = RocList::from_iter(bytes); - - roc_http::ResponseToAndFromHost { - body, - status, - headers, - } - }, - Err(_) => { - roc_http::ResponseToAndFromHost { - status: 500, - headers: RocList::empty(), - body: roc_http::REQUEST_BAD_BODY.into(), - } - } - } - } - Err(err) => { - // TODO match on the error type to provide more specific responses with appropriate status codes - /*use std::error::Error; - let err_source_opt = err.source();*/ - - roc_http::ResponseToAndFromHost { - status: 500, - headers: RocList::empty(), - body: format!("ERROR:\n{}", err).as_bytes().into(), - } - } - } -} - -#[no_mangle] -pub extern "C" fn roc_fx_tcp_connect(host: &RocStr, port: u16) -> RocResult, RocStr> { - roc_http::tcp_connect(host, port) -} - -#[no_mangle] -pub extern "C" fn roc_fx_tcp_read_up_to( - stream: RocBox<()>, - bytes_to_read: u64, -) -> RocResult, RocStr> { - roc_http::tcp_read_up_to(stream, bytes_to_read) -} - -#[no_mangle] -pub extern "C" fn roc_fx_tcp_read_exactly( - stream: RocBox<()>, - bytes_to_read: u64, -) -> RocResult, RocStr> { - roc_http::tcp_read_exactly(stream, bytes_to_read) -} - -#[no_mangle] -pub extern "C" fn roc_fx_tcp_read_until( - stream: RocBox<()>, - byte: u8, -) -> RocResult, RocStr> { - roc_http::tcp_read_until(stream, byte) -} - -#[no_mangle] -pub extern "C" fn roc_fx_tcp_write(stream: RocBox<()>, msg: &RocList) -> RocResult<(), RocStr> { - roc_http::tcp_write(stream, msg) -} - -#[no_mangle] -pub extern "C" fn roc_fx_command_exec_exit_code( - roc_cmd: &roc_command::Command, -) -> RocResult { - roc_command::command_exec_exit_code(roc_cmd) -} - -#[no_mangle] -pub extern "C" fn roc_fx_command_exec_output( - roc_cmd: &roc_command::Command, -) -> RocResult> { - roc_command::command_exec_output(roc_cmd) -} - -#[no_mangle] -pub extern "C" fn roc_fx_dir_create(roc_path: &RocList) -> RocResult<(), roc_io_error::IOErr> { - roc_file::dir_create(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_dir_create_all( - roc_path: &RocList, -) -> RocResult<(), roc_io_error::IOErr> { - roc_file::dir_create_all(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_dir_delete_empty( - roc_path: &RocList, -) -> RocResult<(), roc_io_error::IOErr> { - roc_file::dir_delete_empty(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_dir_delete_all( - roc_path: &RocList, -) -> RocResult<(), roc_io_error::IOErr> { - roc_file::dir_delete_all(roc_path) -} - -#[no_mangle] -pub extern "C" fn roc_fx_hard_link( - path_original: &RocList, - path_link: &RocList, -) -> RocResult<(), roc_io_error::IOErr> { - roc_file::hard_link(path_original, path_link) -} - -#[no_mangle] -pub extern "C" fn roc_fx_current_arch_os() -> roc_env::ReturnArchOS { - roc_env::current_arch_os() -} - -#[no_mangle] -pub extern "C" fn roc_fx_temp_dir() -> RocList { - roc_env::temp_dir() -} - -#[no_mangle] -pub extern "C" fn roc_fx_get_locale() -> RocResult { - roc_env::get_locale() -} - -#[no_mangle] -pub extern "C" fn roc_fx_get_locales() -> RocList { - roc_env::get_locales() -} - -#[no_mangle] -pub extern "C" fn roc_fx_random_u64() -> RocResult { - roc_random::random_u64() -} - -#[no_mangle] -pub extern "C" fn roc_fx_random_u32() -> RocResult { - roc_random::random_u32() -} - -#[no_mangle] -pub extern "C" fn roc_fx_sqlite_bind( - stmt: RocBox<()>, - bindings: &RocList, -) -> RocResult<(), roc_sqlite::SqliteError> { - roc_sqlite::bind(stmt, bindings) -} - -#[no_mangle] -pub extern "C" fn roc_fx_sqlite_prepare( - db_path: &roc_std::RocStr, - query: &roc_std::RocStr, -) -> roc_std::RocResult, roc_sqlite::SqliteError> { - roc_sqlite::prepare(db_path, query) -} - -#[no_mangle] -pub extern "C" fn roc_fx_sqlite_columns(stmt: RocBox<()>) -> RocList { - roc_sqlite::columns(stmt) -} - -#[no_mangle] -pub extern "C" fn roc_fx_sqlite_column_value( - stmt: RocBox<()>, - i: u64, -) -> RocResult { - roc_sqlite::column_value(stmt, i) -} - -#[no_mangle] -pub extern "C" fn roc_fx_sqlite_step( - stmt: RocBox<()>, -) -> RocResult { - roc_sqlite::step(stmt) -} - -/// Resets a prepared statement back to its initial state, ready to be re-executed. -#[no_mangle] -pub extern "C" fn roc_fx_sqlite_reset(stmt: RocBox<()>) -> RocResult<(), roc_sqlite::SqliteError> { - roc_sqlite::reset(stmt) -} diff --git a/crates/roc_host_bin/Cargo.toml b/crates/roc_host_bin/Cargo.toml deleted file mode 100644 index 1167a897..00000000 --- a/crates/roc_host_bin/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "roc_host_bin" -version = "0.0.1" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -description = "This crate wraps roc_host to build an executable. This executable is used by `roc preprocess-host ...`. That command generates an .rh and .rm file, these files are used by the [surgical linker](https://github.com/roc-lang/roc/tree/main/crates/linker#the-roc-surgical-linker)." - -[[bin]] -name = "host" -path = "src/main.rs" - -[dependencies] -roc_std.workspace = true -roc_host.workspace = true -roc_env.workspace = true diff --git a/crates/roc_host_bin/build.rs b/crates/roc_host_bin/build.rs deleted file mode 100644 index dedb2aef..00000000 --- a/crates/roc_host_bin/build.rs +++ /dev/null @@ -1,6 +0,0 @@ -fn main() { - // Make sure we have enough free space for additional load commands - // that we expect the surgical linker to add to the preprocessed host. - #[cfg(target_os = "macos")] - println!("cargo:rustc-link-arg=-Wl,-headerpad,0x1000") -} diff --git a/crates/roc_host_bin/src/main.rs b/crates/roc_host_bin/src/main.rs deleted file mode 100644 index 18e73812..00000000 --- a/crates/roc_host_bin/src/main.rs +++ /dev/null @@ -1,9 +0,0 @@ -use roc_env::arg::ArgToAndFromHost; - -fn main() { - let args = std::env::args_os().map(ArgToAndFromHost::from).collect(); - - let exit_code = roc_host::rust_main(args); - - std::process::exit(exit_code); -} diff --git a/crates/roc_host_lib/Cargo.toml b/crates/roc_host_lib/Cargo.toml deleted file mode 100644 index aba09433..00000000 --- a/crates/roc_host_lib/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "host" -version = "0.0.1" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -description = "This crate wraps roc_host and produces a static library (.a file). This .a file is used for legacy linking. Legacy linking refers to using a typical linker like ld or ldd instead of the Roc surgical linker." - -[lib] -name = "host" -path = "src/lib.rs" -crate-type = ["staticlib"] - -[dependencies] -roc_std.workspace = true -roc_host.workspace = true -roc_env.workspace = true diff --git a/crates/roc_host_lib/src/lib.rs b/crates/roc_host_lib/src/lib.rs deleted file mode 100644 index 17371edd..00000000 --- a/crates/roc_host_lib/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -use roc_env::arg::ArgToAndFromHost; -use std::ffi::c_char; - -/// # Safety -/// This function is the entry point for the program, it will be linked by roc using the legacy linker -/// to produce the final executable. -/// -/// Note we use argc and argv to pass arguments to the program instead of std::env::args(). -#[no_mangle] -pub unsafe extern "C" fn main(argc: usize, argv: *const *const c_char) -> i32 { - let args = std::slice::from_raw_parts(argv, argc) - .iter() - .map(|&c_ptr| { - let c_str = std::ffi::CStr::from_ptr(c_ptr); - - ArgToAndFromHost::from(c_str.to_bytes()) - }) - .collect(); - - // return exit_code - roc_host::rust_main(args) -} diff --git a/crates/roc_http/Cargo.toml b/crates/roc_http/Cargo.toml deleted file mode 100644 index 3eb6900b..00000000 --- a/crates/roc_http/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "roc_http" -description = "Common functionality for Roc to interface with hyper" - -authors.workspace = true -edition.workspace = true -license.workspace = true -version.workspace = true - -[dependencies] -roc_std.workspace = true -roc_std_heap.workspace = true -roc_io_error.workspace = true -roc_file.workspace = true -memchr.workspace = true -hyper.workspace = true -hyper-rustls.workspace = true -tokio.workspace = true -bytes.workspace = true -http-body-util.workspace = true diff --git a/crates/roc_http/src/lib.rs b/crates/roc_http/src/lib.rs deleted file mode 100644 index 537b1e46..00000000 --- a/crates/roc_http/src/lib.rs +++ /dev/null @@ -1,350 +0,0 @@ -//! This crate provides common functionality for Roc to interface with `std::net::tcp` -use roc_std::{RocBox, RocList, RocRefcounted, RocResult, RocStr}; -use roc_std_heap::ThreadSafeRefcountedResourceHeap; -use std::env; -use std::io::{BufRead, BufReader, ErrorKind, Read, Write}; -use std::net::TcpStream; -use std::sync::OnceLock; -use bytes::Bytes; - -pub const REQUEST_TIMEOUT_BODY: &[u8] = "RequestTimeout".as_bytes(); -pub const REQUEST_NETWORK_ERR: &[u8] = "Network Error".as_bytes(); -pub const REQUEST_BAD_BODY: &[u8] = "Bad Body".as_bytes(); - -pub fn heap() -> &'static ThreadSafeRefcountedResourceHeap> { - // TODO: Should this be a BufReader and BufWriter of the tcp stream? - // like this: https://stackoverflow.com/questions/58467659/how-to-store-tcpstream-with-bufreader-and-bufwriter-in-a-data-structure/58491889#58491889 - - static TCP_HEAP: OnceLock>> = - OnceLock::new(); - TCP_HEAP.get_or_init(|| { - let default_max = 65536; - let max_tcp_streams = env::var("ROC_BASIC_CLI_MAX_TCP_STREAMS") - .map(|v| v.parse().unwrap_or(default_max)) - .unwrap_or(default_max); - ThreadSafeRefcountedResourceHeap::new(max_tcp_streams) - .expect("Failed to allocate mmap for tcp handle references.") - }) -} - -const UNEXPECTED_EOF_ERROR: &str = "UnexpectedEof"; - -#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C, align(8))] -pub struct RequestToAndFromHost { - pub body: RocList, - pub headers: RocList
, - pub method: u64, - pub method_ext: RocStr, - pub timeout_ms: u64, - pub uri: RocStr, -} - -impl RocRefcounted for RequestToAndFromHost { - fn inc(&mut self) { - self.body.inc(); - self.headers.inc(); - self.method_ext.inc(); - self.uri.inc(); - } - fn dec(&mut self) { - self.body.dec(); - self.headers.dec(); - self.method_ext.dec(); - self.uri.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -impl From> for RequestToAndFromHost { - fn from(hyper_req: hyper::Request) -> RequestToAndFromHost { - let body = RocList::from(hyper_req.body().as_bytes()); - let headers = RocList::from_iter(hyper_req.headers().iter().map(|(key, value)| { - Header::new( - RocStr::from(key.as_str()), - RocStr::from(value.to_str().unwrap()), - ) - })); - let method: u64 = RequestToAndFromHost::from_hyper_method(hyper_req.method()); - let method_ext = { - if RequestToAndFromHost::is_extension_method(method) { - RocStr::from(hyper_req.method().as_str()) - } else { - RocStr::empty() - } - }; - let timeout_ms = 0; // request is from server... roc hasn't got a timeout - let uri = hyper_req.uri().to_string().as_str().into(); - - RequestToAndFromHost { - body, - headers, - method, - method_ext, - timeout_ms, - uri, - } - } -} - -impl RequestToAndFromHost { - pub fn has_timeout(&self) -> Option { - if self.timeout_ms > 0 { - Some(self.timeout_ms) - } else { - None - } - } - - pub fn is_extension_method(raw_method: u64) -> bool { - raw_method == 2 - } - - pub fn from_hyper_method(method: &hyper::Method) -> u64 { - match *method { - hyper::Method::CONNECT => 0, - hyper::Method::DELETE => 1, - hyper::Method::GET => 3, - hyper::Method::HEAD => 4, - hyper::Method::OPTIONS => 5, - hyper::Method::PATCH => 6, - hyper::Method::POST => 7, - hyper::Method::PUT => 8, - hyper::Method::TRACE => 9, - _ => 2, - } - } - - pub fn as_hyper_method(&self) -> hyper::Method { - match self.method { - 0 => hyper::Method::CONNECT, - 1 => hyper::Method::DELETE, - 2 => hyper::Method::from_bytes(self.method_ext.as_bytes()).unwrap(), - 3 => hyper::Method::GET, - 4 => hyper::Method::HEAD, - 5 => hyper::Method::OPTIONS, - 6 => hyper::Method::PATCH, - 7 => hyper::Method::POST, - 8 => hyper::Method::PUT, - 9 => hyper::Method::TRACE, - _ => panic!("invalid method"), - } - } - - pub fn to_hyper_request(&self) -> Result>, hyper::http::Error> { - let method: hyper::Method = self.as_hyper_method(); - let mut req_builder = hyper::Request::builder() - .method(method) - .uri(self.uri.as_str()); - - // we will give a default content type if the user hasn't - // set one in the provided headers - let mut has_content_type_header = false; - - for header in self.headers.iter() { - req_builder = req_builder.header(header.name.as_str(), header.value.as_str()); - if header.name.eq_ignore_ascii_case("Content-Type") { - has_content_type_header = true; - } - } - - if !has_content_type_header { - req_builder = req_builder.header("Content-Type", "text/plain"); - } - - let bytes: http_body_util::Full = http_body_util::Full::new(self.body.as_slice().to_vec().into()); - - req_builder.body(bytes) - } -} - -#[derive(Clone, Default, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C)] -pub struct ResponseToAndFromHost { - pub body: RocList, - pub headers: RocList
, - pub status: u16, -} - -impl RocRefcounted for ResponseToAndFromHost { - fn inc(&mut self) { - self.body.inc(); - self.headers.inc(); - } - fn dec(&mut self) { - self.body.dec(); - self.headers.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -impl From for ResponseToAndFromHost { - fn from(err: hyper::http::Error) -> Self { - ResponseToAndFromHost { - status: 500, - headers: RocList::empty(), - body: err.to_string().as_bytes().into(), - } - } -} - -impl From for hyper::Response> { - fn from(roc_response: ResponseToAndFromHost) -> Self { - let mut builder = hyper::Response::builder(); - - // TODO handle invalid status code provided from roc.... - // we should return an error - builder = builder.status( - hyper::StatusCode::from_u16(roc_response.status).expect("valid status from roc"), - ); - - for header in roc_response.headers.iter() { - builder = builder.header(header.name.as_str(), header.value.as_bytes()); - } - - builder - .body(http_body_util::Full::new(Vec::from(roc_response.body.as_slice()).into())) // TODO try not to use Vec here - .unwrap() // TODO don't unwrap this - } -} - -impl From for hyper::StatusCode { - fn from(response: ResponseToAndFromHost) -> Self { - hyper::StatusCode::from_u16(response.status).expect("valid status code from roc") - } -} - -#[derive(Clone, Default, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C)] -pub struct Header { - pub name: RocStr, - pub value: RocStr, -} - -impl Header { - pub fn new>(name: T, value: T) -> Header { - Header { - name: name.into(), - value: value.into(), - } - } -} - -impl roc_std::RocRefcounted for Header { - fn inc(&mut self) { - self.name.inc(); - self.value.inc(); - } - fn dec(&mut self) { - self.name.dec(); - self.value.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -pub fn tcp_connect(host: &RocStr, port: u16) -> RocResult, RocStr> { - match TcpStream::connect((host.as_str(), port)) { - Ok(stream) => { - let buf_reader = BufReader::new(stream); - - let heap = heap(); - let alloc_result = heap.alloc_for(buf_reader); - match alloc_result { - Ok(out) => RocResult::ok(out), - Err(err) => RocResult::err(to_tcp_connect_err(err)), - } - } - Err(err) => RocResult::err(to_tcp_connect_err(err)), - } -} - -pub fn tcp_read_up_to(stream: RocBox<()>, bytes_to_read: u64) -> RocResult, RocStr> { - let stream: &mut BufReader = - ThreadSafeRefcountedResourceHeap::box_to_resource(stream); - - let mut chunk = stream.take(bytes_to_read); - - //TODO: fill a roc list directly. This is an extra O(n) copy. - match chunk.fill_buf() { - Ok(received) => { - let received = received.to_vec(); - stream.consume(received.len()); - - RocResult::ok(RocList::from(&received[..])) - } - Err(err) => RocResult::err(to_tcp_stream_err(err)), - } -} - -pub fn tcp_read_exactly(stream: RocBox<()>, bytes_to_read: u64) -> RocResult, RocStr> { - let stream: &mut BufReader = - ThreadSafeRefcountedResourceHeap::box_to_resource(stream); - - let mut buffer = Vec::with_capacity(bytes_to_read as usize); - let mut chunk = stream.take(bytes_to_read); - - //TODO: fill a roc list directly. This is an extra O(n) copy. - match chunk.read_to_end(&mut buffer) { - Ok(read) => { - if (read as u64) < bytes_to_read { - RocResult::err(UNEXPECTED_EOF_ERROR.into()) - } else { - RocResult::ok(RocList::from(&buffer[..])) - } - } - Err(err) => RocResult::err(to_tcp_stream_err(err)), - } -} - -pub fn tcp_read_until(stream: RocBox<()>, byte: u8) -> RocResult, RocStr> { - let stream: &mut BufReader = - ThreadSafeRefcountedResourceHeap::box_to_resource(stream); - - let mut buffer = RocList::empty(); - match roc_file::read_until(stream, byte, &mut buffer) { - Ok(_) => RocResult::ok(buffer), - Err(err) => RocResult::err(to_tcp_stream_err(err)), - } -} - -pub fn tcp_write(stream: RocBox<()>, msg: &RocList) -> RocResult<(), RocStr> { - let stream: &mut BufReader = - ThreadSafeRefcountedResourceHeap::box_to_resource(stream); - - match stream.get_mut().write_all(msg.as_slice()) { - Ok(()) => RocResult::ok(()), - Err(err) => RocResult::err(to_tcp_stream_err(err)), - } -} - -// TODO replace with IOErr -fn to_tcp_connect_err(err: std::io::Error) -> RocStr { - match err.kind() { - ErrorKind::PermissionDenied => "ErrorKind::PermissionDenied".into(), - ErrorKind::AddrInUse => "ErrorKind::AddrInUse".into(), - ErrorKind::AddrNotAvailable => "ErrorKind::AddrNotAvailable".into(), - ErrorKind::ConnectionRefused => "ErrorKind::ConnectionRefused".into(), - ErrorKind::Interrupted => "ErrorKind::Interrupted".into(), - ErrorKind::TimedOut => "ErrorKind::TimedOut".into(), - ErrorKind::Unsupported => "ErrorKind::Unsupported".into(), - other => format!("{:?}", other).as_str().into(), - } -} - -fn to_tcp_stream_err(err: std::io::Error) -> RocStr { - match err.kind() { - ErrorKind::PermissionDenied => "ErrorKind::PermissionDenied".into(), - ErrorKind::ConnectionRefused => "ErrorKind::ConnectionRefused".into(), - ErrorKind::ConnectionReset => "ErrorKind::ConnectionReset".into(), - ErrorKind::Interrupted => "ErrorKind::Interrupted".into(), - ErrorKind::OutOfMemory => "ErrorKind::OutOfMemory".into(), - ErrorKind::BrokenPipe => "ErrorKind::BrokenPipe".into(), - other => format!("{:?}", other).as_str().into(), - } -} diff --git a/crates/roc_io_error/Cargo.toml b/crates/roc_io_error/Cargo.toml deleted file mode 100644 index cadd82e5..00000000 --- a/crates/roc_io_error/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "roc_io_error" -description = "Common functionality for Roc to interface with std::io::Error" - -authors.workspace = true -edition.workspace = true -license.workspace = true -version.workspace = true - -[dependencies] -roc_std.workspace = true -roc_std_heap.workspace = true diff --git a/crates/roc_io_error/src/lib.rs b/crates/roc_io_error/src/lib.rs deleted file mode 100644 index 20071ea9..00000000 --- a/crates/roc_io_error/src/lib.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! This crate provides common functionality for Roc to interface with `std::io::Error` -use roc_std::{roc_refcounted_noop_impl, RocRefcounted, RocStr}; - -#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(u8)] -pub enum IOErrTag { - AlreadyExists = 0, - BrokenPipe = 1, - EndOfFile = 2, - Interrupted = 3, - NotFound = 4, - Other = 5, - OutOfMemory = 6, - PermissionDenied = 7, - Unsupported = 8, -} - -impl core::fmt::Debug for IOErrTag { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::AlreadyExists => f.write_str("IOErrTag::AlreadyExists"), - Self::BrokenPipe => f.write_str("IOErrTag::BrokenPipe"), - Self::EndOfFile => f.write_str("IOErrTag::EndOfFile"), - Self::Interrupted => f.write_str("IOErrTag::Interrupted"), - Self::NotFound => f.write_str("IOErrTag::NotFound"), - Self::Other => f.write_str("IOErrTag::Other"), - Self::OutOfMemory => f.write_str("IOErrTag::OutOfMemory"), - Self::PermissionDenied => f.write_str("IOErrTag::PermissionDenied"), - Self::Unsupported => f.write_str("IOErrTag::Unsupported"), - } - } -} - -roc_refcounted_noop_impl!(IOErrTag); - -#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C)] -pub struct IOErr { - pub msg: roc_std::RocStr, - pub tag: IOErrTag, -} - -impl roc_std::RocRefcounted for IOErr { - fn inc(&mut self) { - self.msg.inc(); - } - fn dec(&mut self) { - self.msg.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -impl From for IOErr { - fn from(e: std::io::Error) -> Self { - let other = || -> IOErr { - IOErr { - tag: IOErrTag::Other, - msg: format!("{}", e).as_str().into(), - } - }; - - let with_empty_msg = |tag: IOErrTag| -> IOErr { - IOErr { - tag, - msg: RocStr::empty(), - } - }; - - match e.kind() { - std::io::ErrorKind::NotFound => with_empty_msg(IOErrTag::NotFound), - std::io::ErrorKind::PermissionDenied => with_empty_msg(IOErrTag::PermissionDenied), - std::io::ErrorKind::BrokenPipe => with_empty_msg(IOErrTag::BrokenPipe), - std::io::ErrorKind::AlreadyExists => with_empty_msg(IOErrTag::AlreadyExists), - std::io::ErrorKind::Interrupted => with_empty_msg(IOErrTag::Interrupted), - std::io::ErrorKind::Unsupported => with_empty_msg(IOErrTag::Unsupported), - std::io::ErrorKind::OutOfMemory => with_empty_msg(IOErrTag::OutOfMemory), - _ => other(), - } - } -} diff --git a/crates/roc_random/Cargo.toml b/crates/roc_random/Cargo.toml deleted file mode 100644 index 5bfe5dbf..00000000 --- a/crates/roc_random/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "roc_random" -description = "Common functionality for Roc to interface with getrandom" -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -version.workspace = true - -[dependencies] -roc_std.workspace = true -roc_io_error.workspace = true -getrandom.workspace = true diff --git a/crates/roc_random/src/lib.rs b/crates/roc_random/src/lib.rs deleted file mode 100644 index 8187b5b4..00000000 --- a/crates/roc_random/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -use roc_std::{RocList, RocResult}; -use roc_io_error::IOErr; - - -pub fn random_u64() -> RocResult { - getrandom::u64() - .map_err(|e| std::io::Error::from(e)) - .map_err(|e| IOErr::from(e)) - .into() -} - -pub fn random_u32() -> RocResult { - getrandom::u32() - .map_err(|e| std::io::Error::from(e)) - .map_err(|e| IOErr::from(e)) - .into() -} diff --git a/crates/roc_sqlite/Cargo.toml b/crates/roc_sqlite/Cargo.toml deleted file mode 100644 index a92e8686..00000000 --- a/crates/roc_sqlite/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "roc_sqlite" -description = "Common functionality for Roc to interface with sqlite" - -authors.workspace = true -edition.workspace = true -license.workspace = true -version.workspace = true - -[dependencies] -roc_std.workspace = true -roc_std_heap.workspace = true -libsqlite3-sys.workspace = true -thread_local.workspace = true diff --git a/crates/roc_sqlite/src/lib.rs b/crates/roc_sqlite/src/lib.rs deleted file mode 100644 index e5689db9..00000000 --- a/crates/roc_sqlite/src/lib.rs +++ /dev/null @@ -1,581 +0,0 @@ -//! This crate provides common functionality common functionality for Roc to interface with sqlite. -#![allow(non_snake_case)] - -use roc_std::{roc_refcounted_noop_impl, RocBox, RocList, RocRefcounted, RocResult, RocStr}; -use roc_std_heap::ThreadSafeRefcountedResourceHeap; -use std::borrow::Borrow; -use std::cell::RefCell; -use std::ffi::{c_char, c_int, c_void, CStr, CString}; -use std::sync::OnceLock; -use thread_local::ThreadLocal; - -pub fn heap() -> &'static ThreadSafeRefcountedResourceHeap { - static STMT_HEAP: OnceLock> = OnceLock::new(); - STMT_HEAP.get_or_init(|| { - let default_max_stmts = 65536; - let max_stmts = std::env::var("ROC_BASIC_CLI_MAX_SQLITE_STMTS") - .map(|v| v.parse().unwrap_or(default_max_stmts)) - .unwrap_or(default_max_stmts); - ThreadSafeRefcountedResourceHeap::new(max_stmts) - .expect("Failed to allocate mmap for sqlite statement handle references.") - }) -} - -type SqliteConnection = *mut libsqlite3_sys::sqlite3; - -// We are guaranteeing that we are using these on single threads. -// This keeps them thread safe. -#[repr(transparent)] -struct UnsafeStmt(*mut libsqlite3_sys::sqlite3_stmt); - -unsafe impl Send for UnsafeStmt {} -unsafe impl Sync for UnsafeStmt {} - -// This will lazily prepare an sqlite connection on each thread. -pub struct SqliteStatement { - db_path: RocStr, - query: RocStr, - stmt: ThreadLocal, -} - -impl Drop for SqliteStatement { - fn drop(&mut self) { - for stmt in self.stmt.iter() { - unsafe { libsqlite3_sys::sqlite3_finalize(stmt.0) }; - } - } -} - -thread_local! { - // TODO: Once roc has atomic refcounts and sharing between threads, this really should be managed by roc. - // We should have a heap of connections just like statements. - // Each statement will need to keep a reference to the connection it uses. - // Connections will still need some sort of thread local to enable multithread access (connection per thread). - static SQLITE_CONNECTIONS : RefCell> = const { RefCell::new(vec![]) }; -} - -fn get_connection(path: &str) -> Result { - SQLITE_CONNECTIONS.with(|connections| { - for (conn_path, connection) in connections.borrow().iter() { - if path.as_bytes() == conn_path.as_c_str().to_bytes() { - return Ok(*connection); - } - } - - let path = CString::new(path).unwrap(); - let mut connection: SqliteConnection = std::ptr::null_mut(); - // TODO: we should eventually allow users to decide if they want to create a database. - // This is errorprone and can lead to creating a database when the user wants to open a existing one. - let flags = libsqlite3_sys::SQLITE_OPEN_CREATE - | libsqlite3_sys::SQLITE_OPEN_READWRITE - | libsqlite3_sys::SQLITE_OPEN_NOMUTEX; - let err = unsafe { - libsqlite3_sys::sqlite3_open_v2(path.as_ptr(), &mut connection, flags, std::ptr::null()) - }; - if err != libsqlite3_sys::SQLITE_OK { - return Err(err_from_sqlite_conn(connection, err)); - } - - connections.borrow_mut().push((path, connection)); - Ok(connection) - }) -} - -fn thread_local_prepare( - stmt: &SqliteStatement, -) -> Result<*mut libsqlite3_sys::sqlite3_stmt, SqliteError> { - // Get the connection - let connection = { - match get_connection(stmt.db_path.as_str()) { - Ok(conn) => conn, - Err(err) => return Err(err), - } - }; - - stmt.stmt - .get_or_try(|| { - let mut unsafe_stmt = UnsafeStmt(std::ptr::null_mut()); - let err = unsafe { - libsqlite3_sys::sqlite3_prepare_v2( - connection, - stmt.query.as_str().as_ptr() as *const c_char, - stmt.query.len() as i32, - &mut unsafe_stmt.0, - std::ptr::null_mut(), - ) - }; - if err != libsqlite3_sys::SQLITE_OK { - return Err(err_from_sqlite_conn(connection, err)); - } - Ok(unsafe_stmt) - }) - .map(|x| x.0) -} - -pub fn prepare( - db_path: &roc_std::RocStr, - query: &roc_std::RocStr, -) -> roc_std::RocResult, SqliteError> { - // Prepare the query - let stmt = SqliteStatement { - db_path: db_path.clone(), - query: query.clone(), - stmt: ThreadLocal::new(), - }; - - // Always prepare once to ensure no errors and prep for current thread. - if let Err(err) = thread_local_prepare(&stmt) { - return RocResult::err(err); - } - - let heap = heap(); - let alloc_result = heap.alloc_for(stmt); - match alloc_result { - Ok(out) => RocResult::ok(out), - Err(_) => RocResult::err(SqliteError { - code: libsqlite3_sys::SQLITE_NOMEM as i64, - message: "Ran out of memory allocating space for statement".into(), - }), - } -} - -pub fn bind(stmt: RocBox<()>, bindings: &RocList) -> RocResult<(), SqliteError> { - let stmt: &SqliteStatement = ThreadSafeRefcountedResourceHeap::box_to_resource(stmt); - - let local_stmt = thread_local_prepare(stmt) - .expect("Prepare already succeeded in another thread. Should not fail here"); - - // Clear old bindings to ensure the users is setting all bindings - let err = unsafe { libsqlite3_sys::sqlite3_clear_bindings(local_stmt) }; - if err != libsqlite3_sys::SQLITE_OK { - return roc_err_from_sqlite_errcode(stmt, err); - } - - for binding in bindings { - // TODO: if there is extra capacity in the roc str, zero a byte and use the roc str directly. - let name = CString::new(binding.name.as_str()).unwrap(); - let index = - unsafe { libsqlite3_sys::sqlite3_bind_parameter_index(local_stmt, name.as_ptr()) }; - if index == 0 { - return RocResult::err(SqliteError { - code: libsqlite3_sys::SQLITE_ERROR as i64, - message: RocStr::from(format!("unknown paramater: {:?}", name).as_str()), - }); - } - let err = match binding.value.discriminant() { - SqliteValueDiscriminant::Integer => unsafe { - libsqlite3_sys::sqlite3_bind_int64( - local_stmt, - index, - binding.value.borrow_Integer(), - ) - }, - SqliteValueDiscriminant::Real => unsafe { - libsqlite3_sys::sqlite3_bind_double(local_stmt, index, binding.value.borrow_Real()) - }, - SqliteValueDiscriminant::String => unsafe { - let str = binding.value.borrow_String().as_str(); - let transient = std::mem::transmute::< - *const std::ffi::c_void, - unsafe extern "C" fn(*mut std::ffi::c_void), - >(-1isize as *const c_void); - libsqlite3_sys::sqlite3_bind_text64( - local_stmt, - index, - str.as_ptr() as *const c_char, - str.len() as u64, - Some(transient), - libsqlite3_sys::SQLITE_UTF8 as u8, - ) - }, - SqliteValueDiscriminant::Bytes => unsafe { - let str = binding.value.borrow_Bytes().as_slice(); - let transient = std::mem::transmute::< - *const std::ffi::c_void, - unsafe extern "C" fn(*mut std::ffi::c_void), - >(-1isize as *const c_void); - libsqlite3_sys::sqlite3_bind_blob64( - local_stmt, - index, - str.as_ptr() as *const c_void, - str.len() as u64, - Some(transient), - ) - }, - SqliteValueDiscriminant::Null => unsafe { - libsqlite3_sys::sqlite3_bind_null(local_stmt, index) - }, - }; - if err != libsqlite3_sys::SQLITE_OK { - return roc_err_from_sqlite_errcode(stmt, err); - } - } - RocResult::ok(()) -} - -pub fn columns(stmt: RocBox<()>) -> RocList { - let stmt: &SqliteStatement = ThreadSafeRefcountedResourceHeap::box_to_resource(stmt); - - let local_stmt = thread_local_prepare(stmt) - .expect("Prepare already succeeded in another thread. Should not fail here"); - - let count = unsafe { libsqlite3_sys::sqlite3_column_count(local_stmt) } as usize; - let mut list = RocList::with_capacity(count); - for i in 0..count { - let col_name = unsafe { libsqlite3_sys::sqlite3_column_name(local_stmt, i as c_int) }; - let col_name = unsafe { CStr::from_ptr(col_name) }; - // Both of these should be safe. Sqlite should always return a utf8 string with null terminator. - let col_name = RocStr::from(col_name.to_string_lossy().borrow()); - list.append(col_name); - } - list -} - -pub fn column_value(stmt: RocBox<()>, i: u64) -> RocResult { - let stmt: &SqliteStatement = ThreadSafeRefcountedResourceHeap::box_to_resource(stmt); - - let local_stmt = thread_local_prepare(stmt) - .expect("Prepare already succeeded in another thread. Should not fail here"); - - let count = unsafe { libsqlite3_sys::sqlite3_column_count(local_stmt) } as u64; - if i >= count { - return RocResult::err(SqliteError { - code: libsqlite3_sys::SQLITE_ERROR as i64, - message: RocStr::from( - format!("column index out of range: {} of {}", i, count).as_str(), - ), - }); - } - let i = i as i32; - let value = match unsafe { libsqlite3_sys::sqlite3_column_type(local_stmt, i) } { - libsqlite3_sys::SQLITE_INTEGER => { - let val = unsafe { libsqlite3_sys::sqlite3_column_int64(local_stmt, i) }; - SqliteValue::Integer(val) - } - libsqlite3_sys::SQLITE_FLOAT => { - let val = unsafe { libsqlite3_sys::sqlite3_column_double(local_stmt, i) }; - SqliteValue::Real(val) - } - libsqlite3_sys::SQLITE_TEXT => unsafe { - let text = libsqlite3_sys::sqlite3_column_text(local_stmt, i); - let len = libsqlite3_sys::sqlite3_column_bytes(local_stmt, i); - let slice = std::slice::from_raw_parts(text, len as usize); - let val = RocStr::from(std::str::from_utf8_unchecked(slice)); - SqliteValue::String(val) - }, - libsqlite3_sys::SQLITE_BLOB => unsafe { - let blob = libsqlite3_sys::sqlite3_column_blob(local_stmt, i) as *const u8; - let len = libsqlite3_sys::sqlite3_column_bytes(local_stmt, i); - let slice = std::slice::from_raw_parts(blob, len as usize); - let val = RocList::::from(slice); - SqliteValue::Bytes(val) - }, - libsqlite3_sys::SQLITE_NULL => SqliteValue::Null(), - _ => unreachable!(), - }; - RocResult::ok(value) -} - -pub fn step(stmt: RocBox<()>) -> RocResult { - let stmt: &SqliteStatement = ThreadSafeRefcountedResourceHeap::box_to_resource(stmt); - - let local_stmt = thread_local_prepare(stmt) - .expect("Prepare already succeeded in another thread. Should not fail here"); - - let err = unsafe { libsqlite3_sys::sqlite3_step(local_stmt) }; - if err == libsqlite3_sys::SQLITE_ROW { - return RocResult::ok(SqliteState::Row); - } - if err == libsqlite3_sys::SQLITE_DONE { - return RocResult::ok(SqliteState::Done); - } - roc_err_from_sqlite_errcode(stmt, err) -} - -/// Resets a prepared statement back to its initial state, ready to be re-executed. -pub fn reset(stmt: RocBox<()>) -> RocResult<(), SqliteError> { - let stmt: &SqliteStatement = ThreadSafeRefcountedResourceHeap::box_to_resource(stmt); - - let local_stmt = thread_local_prepare(stmt) - .expect("Prepare already succeeded in another thread. Should not fail here"); - - let err = unsafe { libsqlite3_sys::sqlite3_reset(local_stmt) }; - if err != libsqlite3_sys::SQLITE_OK { - return roc_err_from_sqlite_errcode(stmt, err); - } - RocResult::ok(()) -} - -fn roc_err_from_sqlite_errcode( - stmt: &SqliteStatement, - code: c_int, -) -> RocResult { - let mut errstr = - unsafe { CStr::from_ptr(libsqlite3_sys::sqlite3_errstr(code)) }.to_string_lossy(); - // Attempt to grab a more detailed message if it is available. - if let Ok(conn) = get_connection(stmt.db_path.as_str()) { - let errmsg = unsafe { libsqlite3_sys::sqlite3_errmsg(conn) }; - if !errmsg.is_null() { - errstr = unsafe { CStr::from_ptr(errmsg).to_string_lossy() }; - } - } - RocResult::err(SqliteError { - code: code as i64, - message: RocStr::from(errstr.borrow()), - }) -} - -// If a connections fails to be initialized, we have to load the error directly like so. -fn err_from_sqlite_conn(conn: SqliteConnection, code: c_int) -> SqliteError { - let mut errstr = - unsafe { CStr::from_ptr(libsqlite3_sys::sqlite3_errstr(code)) }.to_string_lossy(); - // Attempt to grab a more detailed message if it is available. - let errmsg = unsafe { libsqlite3_sys::sqlite3_errmsg(conn) }; - if !errmsg.is_null() { - errstr = unsafe { CStr::from_ptr(errmsg).to_string_lossy() }; - } - SqliteError { - code: code as i64, - message: RocStr::from(errstr.borrow()), - } -} - -// ========= Underlying Roc Type representations ========== - -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(u8)] -pub enum SqliteState { - Done = 0, - Row = 1, -} - -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(u8)] -pub enum SqliteValueDiscriminant { - Bytes = 0, - Integer = 1, - Null = 2, - Real = 3, - String = 4, -} - -roc_refcounted_noop_impl!(SqliteValueDiscriminant); - -#[repr(C, align(8))] -pub union union_SqliteValue { - Bytes: core::mem::ManuallyDrop>, - Integer: i64, - Null: (), - Real: f64, - String: core::mem::ManuallyDrop, -} - -impl SqliteValue { - /// Returns which variant this tag union holds. Note that this never includes a payload! - pub fn discriminant(&self) -> SqliteValueDiscriminant { - unsafe { - let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self); - - core::mem::transmute::(*bytes.as_ptr().add(24)) - } - } -} - -#[repr(C)] -pub struct SqliteValue { - payload: union_SqliteValue, - discriminant: SqliteValueDiscriminant, -} - -impl SqliteValue { - pub fn unwrap_Bytes(mut self) -> roc_std::RocList { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Bytes); - unsafe { core::mem::ManuallyDrop::take(&mut self.payload.Bytes) } - } - - pub fn borrow_Bytes(&self) -> &roc_std::RocList { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Bytes); - unsafe { self.payload.Bytes.borrow() } - } - - pub fn borrow_mut_Bytes(&mut self) -> &mut roc_std::RocList { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Bytes); - use core::borrow::BorrowMut; - unsafe { self.payload.Bytes.borrow_mut() } - } - - pub fn is_Bytes(&self) -> bool { - matches!(self.discriminant, SqliteValueDiscriminant::Bytes) - } - - pub fn unwrap_Integer(self) -> i64 { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Integer); - unsafe { self.payload.Integer } - } - - pub fn borrow_Integer(&self) -> i64 { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Integer); - unsafe { self.payload.Integer } - } - - pub fn borrow_mut_Integer(&mut self) -> &mut i64 { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Integer); - unsafe { &mut self.payload.Integer } - } - - pub fn is_Integer(&self) -> bool { - matches!(self.discriminant, SqliteValueDiscriminant::Integer) - } - - pub fn is_Null(&self) -> bool { - matches!(self.discriminant, SqliteValueDiscriminant::Null) - } - - pub fn unwrap_Real(self) -> f64 { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Real); - unsafe { self.payload.Real } - } - - pub fn borrow_Real(&self) -> f64 { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Real); - unsafe { self.payload.Real } - } - - pub fn borrow_mut_Real(&mut self) -> &mut f64 { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::Real); - unsafe { &mut self.payload.Real } - } - - pub fn is_Real(&self) -> bool { - matches!(self.discriminant, SqliteValueDiscriminant::Real) - } - - pub fn unwrap_String(mut self) -> roc_std::RocStr { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::String); - unsafe { core::mem::ManuallyDrop::take(&mut self.payload.String) } - } - - pub fn borrow_String(&self) -> &roc_std::RocStr { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::String); - unsafe { self.payload.String.borrow() } - } - - pub fn borrow_mut_String(&mut self) -> &mut roc_std::RocStr { - debug_assert_eq!(self.discriminant, SqliteValueDiscriminant::String); - use core::borrow::BorrowMut; - unsafe { self.payload.String.borrow_mut() } - } - - pub fn is_String(&self) -> bool { - matches!(self.discriminant, SqliteValueDiscriminant::String) - } -} - -impl SqliteValue { - pub fn Bytes(payload: roc_std::RocList) -> Self { - Self { - discriminant: SqliteValueDiscriminant::Bytes, - payload: union_SqliteValue { - Bytes: core::mem::ManuallyDrop::new(payload), - }, - } - } - - pub fn Integer(payload: i64) -> Self { - Self { - discriminant: SqliteValueDiscriminant::Integer, - payload: union_SqliteValue { Integer: payload }, - } - } - - pub fn Null() -> Self { - Self { - discriminant: SqliteValueDiscriminant::Null, - payload: union_SqliteValue { Null: () }, - } - } - - pub fn Real(payload: f64) -> Self { - Self { - discriminant: SqliteValueDiscriminant::Real, - payload: union_SqliteValue { Real: payload }, - } - } - - pub fn String(payload: roc_std::RocStr) -> Self { - Self { - discriminant: SqliteValueDiscriminant::String, - payload: union_SqliteValue { - String: core::mem::ManuallyDrop::new(payload), - }, - } - } -} - -impl Drop for SqliteValue { - fn drop(&mut self) { - // Drop the payloads - match self.discriminant() { - SqliteValueDiscriminant::Bytes => unsafe { - core::mem::ManuallyDrop::drop(&mut self.payload.Bytes) - }, - SqliteValueDiscriminant::Integer => {} - SqliteValueDiscriminant::Null => {} - SqliteValueDiscriminant::Real => {} - SqliteValueDiscriminant::String => unsafe { - core::mem::ManuallyDrop::drop(&mut self.payload.String) - }, - } - } -} - -impl roc_std::RocRefcounted for SqliteValue { - fn inc(&mut self) { - unimplemented!(); - } - fn dec(&mut self) { - unimplemented!(); - } - fn is_refcounted() -> bool { - true - } -} - -#[repr(C)] -pub struct SqliteBindings { - pub name: roc_std::RocStr, - pub value: SqliteValue, -} - -impl RocRefcounted for SqliteBindings { - fn inc(&mut self) { - self.name.inc(); - self.value.inc(); - } - fn dec(&mut self) { - self.name.dec(); - self.value.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(C)] -pub struct SqliteError { - pub code: i64, - pub message: roc_std::RocStr, -} - -impl RocRefcounted for SqliteError { - fn inc(&mut self) { - self.message.inc(); - } - fn dec(&mut self) { - self.message.dec(); - } - fn is_refcounted() -> bool { - true - } -} diff --git a/crates/roc_stdio/Cargo.toml b/crates/roc_stdio/Cargo.toml deleted file mode 100644 index 73be5cec..00000000 --- a/crates/roc_stdio/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "roc_stdio" -description = "Common functionality for Roc to interface with std::io" - -authors.workspace = true -edition.workspace = true -license.workspace = true -version.workspace = true - -[dependencies] -roc_std.workspace = true -roc_io_error.workspace = true diff --git a/crates/roc_stdio/src/lib.rs b/crates/roc_stdio/src/lib.rs deleted file mode 100644 index ed294520..00000000 --- a/crates/roc_stdio/src/lib.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! This crate provides common functionality for Roc to interface with `std::io` -use roc_std::{RocList, RocResult, RocStr}; -use std::io::{BufRead, Read, Write}; - -/// stdinLine! : {} => Result Str IOErr -pub fn stdin_line() -> RocResult { - let stdin = std::io::stdin(); - - match stdin.lock().lines().next() { - None => RocResult::err(roc_io_error::IOErr { - msg: RocStr::empty(), - tag: roc_io_error::IOErrTag::EndOfFile, - }), - Some(Ok(str)) => RocResult::ok(str.as_str().into()), - Some(Err(io_err)) => RocResult::err(io_err.into()), - } -} - -/// stdinBytes! : {} => Result (List U8) IOErr -pub fn stdin_bytes() -> RocResult, roc_io_error::IOErr> { - const BUF_SIZE: usize = 16_384; // 16 KiB = 16 * 1024 = 16,384 bytes - let stdin = std::io::stdin(); - let mut buffer: [u8; BUF_SIZE] = [0; BUF_SIZE]; - - match stdin.lock().read(&mut buffer) { - Ok(bytes_read) => RocResult::ok(RocList::from(&buffer[0..bytes_read])), - Err(io_err) => RocResult::err(io_err.into()), - } -} - -/// stdinReadToEnd! : {} => Result (List U8) IOErr -pub fn stdin_read_to_end() -> RocResult, roc_io_error::IOErr> { - let stdin = std::io::stdin(); - let mut buf = Vec::new(); - match stdin.lock().read_to_end(&mut buf) { - Ok(bytes_read) => RocResult::ok(RocList::from(&buf[0..bytes_read])), - Err(io_err) => RocResult::err(io_err.into()), - } -} - -/// stdoutLine! : Str => Result {} IOErr -pub fn stdout_line(line: &RocStr) -> RocResult<(), roc_io_error::IOErr> { - let stdout = std::io::stdout(); - - let mut handle = stdout.lock(); - - handle - .write_all(line.as_bytes()) - .and_then(|()| handle.write_all("\n".as_bytes())) - .and_then(|()| handle.flush()) - .map_err(|io_err| io_err.into()) - .into() -} - -/// stdoutWrite! : Str => Result {} IOErr -pub fn stdout_write(text: &RocStr) -> RocResult<(), roc_io_error::IOErr> { - let stdout = std::io::stdout(); - let mut handle = stdout.lock(); - - handle - .write_all(text.as_bytes()) - .and_then(|()| handle.flush()) - .map_err(|io_err| io_err.into()) - .into() -} - -pub fn stdout_write_bytes(bytes: &RocList) -> RocResult<(), roc_io_error::IOErr> { - let stdout = std::io::stdout(); - let mut handle = stdout.lock(); - - handle - .write_all(bytes.as_slice()) - .and_then(|()| handle.flush()) - .map_err(|io_err| io_err.into()) - .into() -} - -/// stderrLine! : Str => Result {} IOErr -pub fn stderr_line(line: &RocStr) -> RocResult<(), roc_io_error::IOErr> { - let stderr = std::io::stderr(); - let mut handle = stderr.lock(); - - handle - .write_all(line.as_bytes()) - .and_then(|()| handle.write_all("\n".as_bytes())) - .and_then(|()| handle.flush()) - .map_err(|io_err| io_err.into()) - .into() -} - -/// stderrWrite! : Str => Result {} IOErr -pub fn stderr_write(text: &RocStr) -> RocResult<(), roc_io_error::IOErr> { - let stderr = std::io::stderr(); - let mut handle = stderr.lock(); - - handle - .write_all(text.as_bytes()) - .and_then(|()| handle.flush()) - .map_err(|io_err| io_err.into()) - .into() -} - -pub fn stderr_write_bytes(bytes: &RocList) -> RocResult<(), roc_io_error::IOErr> { - let stderr = std::io::stderr(); - let mut handle = stderr.lock(); - - handle - .write_all(bytes.as_slice()) - .and_then(|()| handle.flush()) - .map_err(|io_err| io_err.into()) - .into() -} diff --git a/examples/README.md b/examples/README.md index 2b8e998c..70d0a305 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,9 +2,16 @@ These are examples of how to make basic CLI (command-line interface) programs using `basic-cli` as a platform. > [!IMPORTANT] -> - If you want to run an example here but don't want to build basic-cli from source; go to the examples of a specific version: https://github.com/roc-lang/basic-cli/tree/0.VERSION.0/examples and in the example; replace the `"../platform/main.roc"` with the release link, for example `"https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br"`. Next, run with `roc ./examples/YOUREXAMPLE.roc`. +> - If you want to run an example here but don't want to build basic-cli from source, use a released bundle URL in place of `"../platform/main.roc"`. > - To use these examples as is with basic-cli built from source you'll need to [build the platform first](https://github.com/roc-lang/basic-cli?tab=readme-ov-file#running-locally). +Every checked-in example should pass `roc check` and `roc build` with the pinned compiler. The CI script enforces this with `./ci/all_tests.sh`. + +Examples that are intentionally kept out of CI while an API or compiler blocker +is tracked use the `.todoroc` extension and must include a TODO comment with a +GitHub issue link. Rename them back to `.roc` only after they check and build +with the pinned compiler. + Feel free to ask [on Zulip](https://roc.zulipchat.com) in the `#beginners` channel if you have questions about making CLI programs, or if there are functions you'd like to see added to this platform (for an application you'd like to build in Roc). diff --git a/examples/bytes-stdin-stdout.roc b/examples/bytes-stdin-stdout.roc index 11ca1a3d..c4ee91db 100644 --- a/examples/bytes-stdin-stdout.roc +++ b/examples/bytes-stdin-stdout.roc @@ -3,13 +3,14 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdin import pf.Stdout import pf.Stderr -import pf.Arg exposing [Arg] +import pf.IOErr exposing [IOErr] # To run this example: check the README.md in this folder -main! : List Arg => Result {} _ -main! = |_args| +main! : List(Str) => Try({}, [EndOfFile, StdinErr(IOErr), StderrErr(IOErr), StdoutErr(IOErr), ..]) +main! = |_args| { data = Stdin.bytes!({})? Stderr.write_bytes!(data)? Stdout.write_bytes!(data)? - Ok {} + Ok({}) +} diff --git a/examples/command-line-args.roc b/examples/command-line-args.roc index 44285b62..f557ec5a 100644 --- a/examples/command-line-args.roc +++ b/examples/command-line-args.roc @@ -1,37 +1,17 @@ -app [main!] { - pf: platform "../platform/main.roc", -} +app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout -import pf.Arg exposing [Arg] - -# How to handle command line arguments in Roc. - -# To run this example: check the README.md in this folder - -main! : List Arg => Result {} _ -main! = |raw_args| - - # get the second argument, the first is the executable's path - when List.get(raw_args, 1) |> Result.map_err(|_| ZeroArgsGiven) is - Err(ZeroArgsGiven) -> - Err(Exit(1, "Error ZeroArgsGiven:\n\tI expected one argument, but I got none.\n\tRun the app like this: `roc main.roc -- input.txt`")) - Ok(first_arg) -> - Stdout.line!("received argument: ${Arg.display(first_arg)}")? - - # # OPTIONAL TIP: - - # If you ever need to pass an arg to a Roc package, it will probably expect an argument with the type `[Unix (List U8), Windows (List U16)]`. - # You can convert an Arg to that with `Arg.to_os_raw`. - # - # Roc packages like to be platform agnostic so that everyone can use them, that's why they avoid platform-specific types like `pf.Arg`. - when Arg.to_os_raw(first_arg) is - Unix(bytes) -> - Stdout.line!("Unix argument, bytes: ${Inspect.to_str(bytes)}")? - - Windows(u16s) -> - Stdout.line!("Windows argument, u16s: ${Inspect.to_str(u16s)}")? - - # You can go back with Arg.from_os_raw: - Stdout.line!("back to Arg: ${Inspect.to_str(Arg.from_os_raw(Arg.to_os_raw(first_arg)))}") \ No newline at end of file +main! = |args| { + # Skip first arg (executable path), get the remaining args + match args.drop_first(1) { + [first_arg, ..] => { + _ = Stdout.line!("received argument: ${first_arg}") + Ok({}) + } + [] => { + _ = Stdout.line!("Error: I expected one argument, but got none.") + Err(Exit(1)) + } + } +} diff --git a/examples/command.roc b/examples/command.roc index 1aaa362b..b082d64e 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -2,50 +2,46 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.Cmd -import pf.Arg exposing [Arg] -# Different ways to run commands like you do in a terminal. +# Different ways to run commands like you do in a terminal. -# To run this example: check the README.md in this folder +main! = |_args| { + # Simplest way to execute a command (prints to your terminal). + Cmd.exec!("echo", ["Hello"])? -main! : List Arg => Result {} _ -main! = |_args| + # To execute and capture the output (stdout and stderr) without inheriting your terminal. + cmd_output = + Cmd.new("echo") + .args(["Hi"]) + .exec_output!()? - # Simplest way to execute a command (prints to your terminal). - Cmd.exec!("echo", ["Hello"])? + _ = Stdout.line!("${Str.inspect(cmd_output)}") - # To execute and capture the output (stdout and stderr) without inheriting your terminal. - cmd_output = - Cmd.new("echo") - |> Cmd.args(["Hi"]) - |> Cmd.exec_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("BAZ", "DUCK") + .env("FOO", "BAR") + .env("XYZ", "ABC") + .exec_cmd!()? - Stdout.line!("${Inspect.to_str(cmd_output)}")? + # 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!()? - # To run a command with environment variables. - Cmd.new("env") - |> Cmd.clear_envs # You probably don't need to clear all other environment variables, this is just an example. - |> Cmd.env("FOO", "BAR") - |> Cmd.envs([("BAZ", "DUCK"), ("XYZ", "ABC")]) # Set multiple environment variables at once with `envs` - |> Cmd.args(["-v"]) - |> Cmd.exec_cmd!()? + _ = Stdout.line!("Exit code: ${exit_code.to_str()}") - # To execute and just get the exit code (prints to your terminal). - # Prefer using `exec!` or `exec_cmd!`. - exit_code = - Cmd.new("cat") - |> Cmd.args(["non_existent.txt"]) - |> Cmd.exec_exit_code!()? + # 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!("Exit code: ${Num.to_str(exit_code)}")? + _ = Stdout.line!("${Str.inspect(cmd_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") - |> Cmd.args(["Hi"]) - |> Cmd.exec_output_bytes!()? - - Stdout.line!("${Inspect.to_str(cmd_output_bytes)}")? - - Ok({}) + Ok({}) +} diff --git a/examples/dir.roc b/examples/dir.roc index a166520a..aba8ce26 100644 --- a/examples/dir.roc +++ b/examples/dir.roc @@ -1,42 +1,46 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout +import pf.Stderr import pf.Dir -import pf.Path -import pf.File -import pf.Arg exposing [Arg] # Demo of all Dir functions. -# To run this example: check the README.md in this folder +main! = |_args| { + dir_result = || { + # Create a directory + Dir.create!("empty-dir")? -main! : List Arg => Result {} _ -main! = |_args| + # Create a directory and its parents + Dir.create_all!("nested-dir/a/b/c")? - # Create a directory - Dir.create!("empty-dir")? + # Create a child directory + Dir.create!("nested-dir/child")? - dir_exists = File.is_dir!("empty-dir")? - expect dir_exists + # List the contents of a directory + paths = Dir.list!("nested-dir")? - # Create a directory and its parents - Dir.create_all!("nested-dir/a/b/c")? + # 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") - # Create a child directory - Dir.create!("nested-dir/child")? + # Delete an empty directory + Dir.delete_empty!("empty-dir")? - # List the contents of a directory - paths_as_str = - Dir.list!("nested-dir") - |> Result.map_ok(|paths| List.map(paths, Path.display))? + # Delete all directories recursively + Dir.delete_all!("nested-dir")? - # Check the contents of the directory - expect Set.from_list(paths_as_str) == Set.from_list(["nested-dir/a", "nested-dir/child"]) + _ = Stdout.line!("Success!") - # Delete an empty directory - Dir.delete_empty!("empty-dir")? + Ok({}) + } - # Delete all directories recursively - Dir.delete_all!("nested-dir")? - - Stdout.line!("Success!") + match dir_result() { + Ok(_) => Ok({}), + Err(err) => { + _ = Stderr.line!("Error during directory operations: ${Str.inspect(err)}") + Err(Exit(1)) + } + } +} diff --git a/examples/env-var.roc b/examples/env-var.roc index a2b99e54..f703b2c4 100644 --- a/examples/env-var.roc +++ b/examples/env-var.roc @@ -1,25 +1,23 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout +import pf.Stderr import pf.Env -import pf.Arg exposing [Arg] -# How to read environment variables with Env.decode +# How to read environment variables with Env.var! -# To run this example: check the README.md in this folder +main! = |_args| { + result = Env.var!("EDITOR") -main! : List Arg => Result {} _ -main! = |_args| + match result { + Ok(editor) => { + _ = Stdout.line!("Your favorite editor is ${editor}!") + Ok({}) + } - editor = Env.decode!("EDITOR")? - - Stdout.line!("Your favorite editor is ${editor}!")? - - # Env.decode! does not return the same type everywhere. - # The type is determined based on type inference. - # Here `Str.join_with` forces the type that Env.decode! returns to be `List Str` - joined_letters = - Env.decode!("LETTERS") - |> Result.map_ok(|letters| Str.join_with(letters, " "))? - - Stdout.line!("Your favorite letters are: ${joined_letters}") + Err(VarNotFound(name)) => { + _ = Stderr.line!("Env var ${name} is not set.") + Ok({}) + } + } +} diff --git a/examples/error-handling.roc b/examples/error-handling.roc index 73261ff9..f7a87af5 100644 --- a/examples/error-handling.roc +++ b/examples/error-handling.roc @@ -1,30 +1,51 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout -import pf.Stderr import pf.File -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder +# Demonstrates error handling patterns -# Demonstrates handling of every possible error - -main! : List Arg => Result {} _ -main! = |_args| +main! = |_args| { file_name = "test-file.txt" - file_read_result = File.read_utf8!(file_name) - - when file_read_result is - Ok(file_content) -> - Stdout.line!("${file_name} contatins: ${file_content}") - - Err(err) -> - err_msg = - when err is - FileReadErr(_, io_err) -> "Error: failed to read file ${file_name} with error:\n\t${Inspect.to_str(io_err)}" - FileReadUtf8Err(_, io_err) -> "Error: file ${file_name} contains invalid UTF-8:\n\t${Inspect.to_str(io_err)}" - - - Stderr.line!(err_msg)? - Err(Exit(1, "")) # non-zero exit code to indicate failure \ No newline at end of file + # 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") + } + } + + # Now demonstrate success path - create, read, then cleanup + file_result = || { + File.write_utf8!(file_name, "Hello from error-handling example!")? + + content = File.read_utf8!(file_name)? + _ = Stdout.line!("${file_name} contains: ${content}") + + # Cleanup + File.delete!(file_name)? + + Ok({}) + } + + match file_result() { + Ok({}) => Ok({}) + Err(_) => { + _ = Stdout.line!("Error during file operations") + Err(Exit(1)) + } + } +} diff --git a/examples/file-accessed-modified-created-time.roc b/examples/file-accessed-modified-created-time.roc index cb2452aa..7f7c39dc 100644 --- a/examples/file-accessed-modified-created-time.roc +++ b/examples/file-accessed-modified-created-time.roc @@ -3,29 +3,23 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.File import pf.Utc -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder -main! : List Arg => Result {} _ -main! = |_args| +main! = |_args| { file = "LICENSE" # NOTE: these functions will not work if basic-cli was built with musl, which is the case for the normal tar.br URL release. # See https://github.com/roc-lang/basic-cli?tab=readme-ov-file#running-locally to build basic-cli without musl. - time_modified = Utc.to_iso_8601(File.time_modified!(file)?) - - time_accessed = Utc.to_iso_8601(File.time_accessed!(file)?) - - time_created = Utc.to_iso_8601(File.time_created!(file)?) - + time_modified = Utc.to_millis_since_epoch(File.time_modified!(file)?) + time_accessed = Utc.to_millis_since_epoch(File.time_accessed!(file)?) + time_created = Utc.to_millis_since_epoch(File.time_created!(file)?) Stdout.line!( - """ - ${file} file time metadata: - Modified: ${time_modified} - Accessed: ${time_accessed} - Created: ${time_created} - """ - ) \ No newline at end of file + \\${file} file time metadata: + \\ Modified: ${time_modified.to_str()} ms since epoch + \\ Accessed: ${time_accessed.to_str()} ms since epoch + \\ Created: ${time_created.to_str()} ms since epoch + ) +} diff --git a/examples/file-permissions.roc b/examples/file-permissions.roc index e2b0c2ee..edcefd7b 100644 --- a/examples/file-permissions.roc +++ b/examples/file-permissions.roc @@ -2,12 +2,10 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.File -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder -main! : List Arg => Result {} _ -main! = |_args| +main! = |_args| { file = "LICENSE" is_executable = File.is_executable!(file)? @@ -17,10 +15,9 @@ main! = |_args| is_writable = File.is_writable!(file)? Stdout.line!( - """ - ${file} file permissions: - Executable: ${Inspect.to_str(is_executable)} - Readable: ${Inspect.to_str(is_readable)} - Writable: ${Inspect.to_str(is_writable)} - """ - ) \ No newline at end of file + \\${file} file permissions: + \\ Executable: ${Str.inspect(is_executable)} + \\ Readable: ${Str.inspect(is_readable)} + \\ Writable: ${Str.inspect(is_writable)} + ) +} diff --git a/examples/file-read-buffered.roc b/examples/file-read-buffered.todoroc similarity index 87% rename from examples/file-read-buffered.roc rename to examples/file-read-buffered.todoroc index 3801760e..d8e739a8 100644 --- a/examples/file-read-buffered.roc +++ b/examples/file-read-buffered.todoroc @@ -1,5 +1,9 @@ app [main!] { pf: platform "../platform/main.roc" } +# TODO(roc-lang/basic-cli#427): skipped during the Roc compiler migration. +# Restore buffered file reader APIs, then rename this file back to .roc once it +# passes roc check/build with the pinned compiler. Related: #214, #215. + import pf.Stdout import pf.File import pf.Arg exposing [Arg] diff --git a/examples/file-read-write.roc b/examples/file-read-write.roc index 13d60cdb..24351536 100644 --- a/examples/file-read-write.roc +++ b/examples/file-read-write.roc @@ -2,30 +2,32 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.File -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder +# Demo of File.read_utf8! and File.write_utf8! -main! : List Arg => Result {} _ -main! = |_args| - # Note: you can also import files directly if you know the path: https://www.roc-lang.org/examples/IngestFiles/README.html +main! = |_args| { out_file = "out.txt" - file_write_read!(out_file)? + _ = Stdout.line!("Writing a string to out.txt") - # Cleanup - File.delete!(out_file) + file_result = || { + File.write_utf8!(out_file, "a string!")? -file_write_read! : Str => Result {} [FileReadErr _ _, FileReadUtf8Err _ _, FileWriteErr _ _, StdoutErr _] -file_write_read! = |file_name| + contents = File.read_utf8!(out_file)? - Stdout.line!("Writing a string to out.txt")? - - File.write_utf8!("a string!", file_name)? - - contents = File.read_utf8!(file_name)? - - Stdout.line!("I read the file back. Its contents are: \"${contents}\"") + _ = Stdout.line!("I read the file back. Its contents are: \"${contents}\"") + # Cleanup + File.delete!(out_file)? + Ok({}) + } + match file_result() { + Ok({}) => Ok({}) + Err(_) => { + _ = Stdout.line!("Error during file operations") + Err(Exit(1)) + } + } +} diff --git a/examples/file-size.roc b/examples/file-size.roc index 854ff76a..9c22061d 100644 --- a/examples/file-size.roc +++ b/examples/file-size.roc @@ -2,12 +2,11 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.File -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder -main! : List Arg => Result {} _ -main! = |_args| +main! = |_args| { file_size = File.size_in_bytes!("LICENSE")? - Stdout.line!("The size of the LICENSE file is: ${Num.to_str(file_size)} bytes") + Stdout.line!("The size of the LICENSE file is: ${file_size.to_str()} bytes") +} diff --git a/examples/hello-world.roc b/examples/hello-world.roc index a4c7ec98..9734e890 100644 --- a/examples/hello-world.roc +++ b/examples/hello-world.roc @@ -1,10 +1,8 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder - -main! : List Arg => Result {} _ -main! = |_args| - Stdout.line!("Hello, World!") +main! = |_args| { + _r = Stdout.line!("Hello, World!") + Ok({}) +} diff --git a/examples/hello.roc b/examples/hello.roc new file mode 100644 index 00000000..c335e37d --- /dev/null +++ b/examples/hello.roc @@ -0,0 +1,9 @@ +app [main!] { pf: platform "../platform/main.roc" } + +import pf.Stdout + +main! : List(Str) => Try({}, [Exit(I32)]) +main! = |_args| { + _ = Stdout.line!("Hello from basic-cli!") + Ok({}) +} diff --git a/examples/http-client.roc b/examples/http-client.roc new file mode 100644 index 00000000..33aee3ed --- /dev/null +++ b/examples/http-client.roc @@ -0,0 +1,52 @@ +app [main!] { pf: platform "../platform/main.roc" } + +import pf.Http +import pf.Stdout + +# Demo of the basic-cli HTTP client against a local server. +# +# To run this example, first start the test server in another terminal: +# +# cd ci/rust_http_server && cargo run --release +# +# then: +# +# roc build examples/http-client.roc +# ./examples/http-client +main! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| + # GET a plain-text body and decode it as UTF-8. + match Http.get_utf8!("http://localhost:9000/utf8test") { + Err(_) => report_failure!("GET /utf8test failed") + Ok(utf8) => { + _ = Stdout.line!("I received '${utf8}' from the server.") + + # GET a JSON body (returned here as the raw response string). + match Http.get_utf8!("http://localhost:9000") { + Err(_) => report_failure!("GET / failed") + Ok(json) => { + _ = Stdout.line!("The json I received was: ${json}") + + # Use send! with a custom header and inspect the Response record. + request = { + ..Http.default_request, + uri: "http://localhost:9000/utf8test", + headers: [Http.header(("Accept", "text/plain"))], + } + match Http.send!(request) { + Ok(response) => { + _ = Stdout.line!("send! returned status ${U16.to_str(response.status)}.") + Ok({}) + } + Err(HttpErr(_)) => report_failure!("send! failed") + } + } + } + } + } + +report_failure! : Str => Try({}, [Exit(I32), ..]) +report_failure! = |message| { + _ = Stdout.line!("HTTP request failed: ${message}") + Ok({}) +} diff --git a/examples/http.roc b/examples/http.todoroc similarity index 89% rename from examples/http.roc rename to examples/http.todoroc index f25907f8..6dd3a7ce 100644 --- a/examples/http.roc +++ b/examples/http.todoroc @@ -3,6 +3,10 @@ app [main!] { json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.13.0/RqendgZw5e1RsQa3kFhgtnMP8efWoqGRsAvubx4-zus.tar.br", } +# TODO(roc-lang/basic-cli#427): skipped during the Roc compiler migration. +# Restore or redesign the HTTP platform API, then rename this file back to .roc +# once it passes roc check/build with the pinned compiler. + import pf.Http import pf.Stdout import json.Json @@ -10,9 +14,8 @@ import pf.Arg exposing [Arg] # Demo of all basic-cli Http functions -# To run this example: +# To run this example: # ``` -# nix develop # cd basic-cli/ci/rust_http_server # cargo run # ``` @@ -60,7 +63,7 @@ main! = |_args| # # Using default_request and providing a header # -------------------------------------------- - + response_2 = Http.default_request |> &uri "https://www.example.com" diff --git a/examples/locale.roc b/examples/locale.roc index fe151722..98337d65 100644 --- a/examples/locale.roc +++ b/examples/locale.roc @@ -1,18 +1,20 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout -import pf.Arg exposing [Arg] import pf.Locale # Getting the preferred locale and all available locales -# To run this example: check the README.md in this folder +main! = |_args| { + locale_str = match Locale.get!() { + Ok(locale) => locale + Err(NotAvailable) => "" + } + match Stdout.line!("The most preferred locale for this system or application: ${locale_str}") { _ => {} } -main! : List Arg => Result {} _ -main! = |_args| - - locale_str = Locale.get!({})? - Stdout.line!("The most preferred locale for this system or application: ${locale_str}")? + all_locales = Locale.all!() + locales_str = Str.join_with(all_locales, ", ") + match Stdout.line!("All available locales for this system or application: [${locales_str}]") { _ => {} } - all_locales = Locale.all!({}) - Stdout.line!("All available locales for this system or application: ${Inspect.to_str(all_locales)}") \ No newline at end of file + Ok({}) +} diff --git a/examples/path.roc b/examples/path.roc index 123ef670..34abf856 100644 --- a/examples/path.roc +++ b/examples/path.roc @@ -2,27 +2,18 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.Path -import pf.Arg exposing [Arg] - -# To run this example: check the README.md in this folder # Demo of basic-cli Path functions -main! : List Arg => Result {} _ -main! = |_args| - +main! = |_args| { path = Path.from_str("path.roc") - a = Path.is_file!(path)? - b = Path.is_dir!(path)? - c = Path.is_sym_link!(path)? - d = Path.type!(path)? - - Stdout.line!( - """ - is_file: ${Inspect.to_str(a)} - is_dir: ${Inspect.to_str(b)} - is_sym_link: ${Inspect.to_str(c)} - type: ${Inspect.to_str(d)} - """ + _ = Stdout.line!( + \\is_file: ${Str.inspect(Path.is_file!(path))} + \\is_dir: ${Str.inspect(Path.is_dir!(path))} + \\is_sym_link: ${Str.inspect(Path.is_sym_link!(path))} + \\display: ${Path.display(path)} ) + + Ok({}) +} diff --git a/examples/print.roc b/examples/print.roc index 433de1c1..1cfe5f53 100644 --- a/examples/print.roc +++ b/examples/print.roc @@ -2,30 +2,26 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.Stderr -import pf.Arg exposing [Arg] # Printing to stdout and stderr -# To run this example: check the README.md in this folder +main! = |_args| { + # Print a string to stdout + match Stdout.line!("Hello, world!") { _ => {} } -main! : List Arg => Result {} _ -main! = |_args| - - # # Print a string to stdout - Stdout.line!("Hello, world!")? + # Print without a newline + match Stdout.write!("No newline after me.") { _ => {} } - # # Print without a newline - Stdout.write!("No newline after me.")? + # Print a string to stderr + match Stderr.line!("Hello, error!") { _ => {} } - # # Print a string to stderr - Stderr.line!("Hello, error!")? + # Print a string to stderr without a newline + match Stderr.write!("Err with no newline after.") { _ => {} } - # # Print a string to stderr without a newline - Stderr.write!("Err with no newline after.")? + # Print a list to stdout + List.for_each!(["Foo", "Bar", "Baz"], |str| { + match Stdout.line!(str) { _ => {} } + }) - # # Print a list to stdout - ["Foo", "Bar", "Baz"] - |> List.for_each_try!(|str| Stdout.line!(str)) - - # Use List.map! if you want to apply an effectful function that returns something. - # Use List.map_try! if you want to apply an effectful function that returns a Result. + Ok({}) +} diff --git a/examples/random.roc b/examples/random.roc index b539f9c8..f4883b2b 100644 --- a/examples/random.roc +++ b/examples/random.roc @@ -1,20 +1,20 @@ app [main!] { pf: platform "../platform/main.roc" } -# To run this example: check the README.md in this folder - # Demo of basic-cli Random functions import pf.Stdout import pf.Random -import pf.Arg exposing [Arg] - -main! : List Arg => Result {} _ -main! = |_args| - random_u64 = Random.random_seed_u64!({})? - Stdout.line!("Random U64 seed is: ${Inspect.to_str(random_u64)}")? - - random_u32 = Random.random_seed_u32!({})? - Stdout.line!("Random U32 seed is: ${Inspect.to_str(random_u32)}") - # See the example linked below on how to generate a sequence of random numbers using a seed - # https://github.com/roc-lang/examples/blob/main/examples/RandomNumbers/main.roc \ No newline at end of file +main! = |_args| { + result = Random.seed_u64!({}) + match result { + Ok(random_u64) => { + _r = Stdout.line!("Random U64 seed is: ${random_u64.to_str()}") + Ok({}) + } + Err(_) => { + _r = Stdout.line!("Failed to generate random seed") + Err(Exit(1)) + } + } +} diff --git a/examples/sqlite-basic.roc b/examples/sqlite-basic.roc index f9c37db2..45e6c9db 100644 --- a/examples/sqlite-basic.roc +++ b/examples/sqlite-basic.roc @@ -3,7 +3,6 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Env import pf.Stdout import pf.Sqlite -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder and set `export DB_PATH=./examples/todos.db` @@ -16,55 +15,70 @@ import pf.Arg exposing [Arg] # status TEXT NOT NULL # ); -main! : List Arg => Result {} _ -main! = |_args| - db_path = Env.var!("DB_PATH")? +main! = |_args| { + db_path = + match Env.var!("DB_PATH") { + Ok(p) => p + Err(_) => "./examples/todos.db" + } todos = query_todos_by_status!(db_path, "todo")? - Stdout.line!("All Todos:")? + _h1 = Stdout.line!("All Todos:") - # print todos - List.for_each_try!( - todos, - |{ id, task, status }| - Stdout.line!("\tid: ${id}, task: ${task}, status: ${Inspect.to_str(status)}"), - )? + List.for_each!(todos, |todo| print_todo!(todo)) completed_todos = query_todos_by_status!(db_path, "completed")? - Stdout.line!("\nCompleted Todos:")? - List.for_each_try!( - completed_todos, - |{ id, task, status }| - Stdout.line!("\tid: ${id}, task: ${task}, status: ${Inspect.to_str(status)}"), - ) + _h2 = Stdout.line!("") + _h3 = Stdout.line!("Completed Todos:") + List.for_each!(completed_todos, |todo| print_todo!(todo)) + Ok({}) +} Todo : { id : Str, status : TodoStatus, task : Str } -query_todos_by_status! : Str, Str => Result (List Todo) (Sqlite.SqlDecodeErr _) +print_todo! = |todo| + match Stdout.line!(" id: ${todo.id}, task: ${todo.task}, status: ${status_to_str(todo.status)}") { + _ => {} + } + query_todos_by_status! = |db_path, status| Sqlite.query_many!( { path: db_path, query: "SELECT id, task, status FROM todos WHERE status = :status;", bindings: [{ name: ":status", value: String(status) }], - # This uses the record builder syntax: https://www.roc-lang.org/examples/RecordBuilder/README.html - rows: { Sqlite.decode_record <- - id: Sqlite.i64("id") |> Sqlite.map_value(Num.to_str), - task: Sqlite.str("task"), - status: Sqlite.str("status") |> Sqlite.map_value_result(decode_todo_status), - }, + rows: decode_todo, }, ) +# A row decoder is `List(Str) -> (Stmt => Try(a, err))`; the new compiler does not +# support the record-builder (`<-`) sugar, so we combine the leaf decoders by hand. +decode_todo = |cols| + |stmt| { + id = Sqlite.i64("id")(cols)(stmt)? + task = Sqlite.str("task")(cols)(stmt)? + status_str = Sqlite.str("status")(cols)(stmt)? + status = decode_todo_status(status_str)? + Ok({ id: I64.to_str(id), task, status }) + } + TodoStatus : [Todo, Completed, InProgress] -decode_todo_status : Str -> Result TodoStatus _ +status_to_str : TodoStatus -> Str +status_to_str = |status| + match status { + Todo => "Todo" + Completed => "Completed" + InProgress => "InProgress" + } + decode_todo_status = |status_str| - when status_str is - "todo" -> Ok(Todo) - "completed" -> Ok(Completed) - "in-progress" -> Ok(InProgress) - _ -> Err(ParseError("Unknown status str: ${status_str}")) \ No newline at end of file + match status_str { + "todo" => Ok(Todo) + "completed" => Ok(Completed) + "in-progress" => Ok(InProgress) + _ => Err(ParseError("Unknown status str: ${status_str}")) + } diff --git a/examples/sqlite-everything.roc b/examples/sqlite-everything.roc index 0bdd2550..121d2c87 100644 --- a/examples/sqlite-everything.roc +++ b/examples/sqlite-everything.roc @@ -3,11 +3,11 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Env import pf.Stdout import pf.Sqlite -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder and set `export DB_PATH=./examples/todos2.db` -# Demo of basic Sqlite usage +# Demo of the wider Sqlite API: queries, decoders, nullable columns, inserts, +# updates, deletes, prepared statements. # Sql that was used to create the table: # CREATE TABLE todos ( @@ -20,68 +20,54 @@ import pf.Arg exposing [Arg] # We recommend using `NOT NULL` when possible. # Note 2: boolean is "fake" in sqlite https://www.sqlite.org/datatype3.html -main! : List Arg => Result {} _ -main! = |_args| - db_path = Env.var!("DB_PATH")? +main! = |_args| { + db_path = + match Env.var!("DB_PATH") { + Ok(p) => p + Err(_) => "./examples/todos2.db" + } # Example: print all rows - - all_todos = Sqlite.query_many!({ - path: db_path, - query: "SELECT * FROM todos;", - bindings: [], - # This uses the record builder syntax: https://www.roc-lang.org/examples/RecordBuilder/README.html - rows: { Sqlite.decode_record <- - id: Sqlite.i64("id"), - task: Sqlite.str("task"), - status: Sqlite.str("status") |> Sqlite.map_value_result(decode_status), - # bools in sqlite are actually integers - edited: Sqlite.nullable_i64("edited") |> Sqlite.map_value(decode_edited), + all_todos = Sqlite.query_many!( + { + path: db_path, + query: "SELECT * FROM todos;", + bindings: [], + rows: decode_full_todo, }, - })? - - Stdout.line!("All Todos:")? - - List.for_each_try!( - all_todos, - |{ id, task, status, edited }| - Stdout.line!("\tid: ${Num.to_str(id)}, task: ${task}, status: ${Inspect.to_str(status)}, edited: ${Inspect.to_str(edited)}"), )? - # Example: filter rows by status + _h1 = Stdout.line!("All Todos:") + List.for_each!(all_todos, |t| print_line!(" id: ${I64.to_str(t.id)}, task: ${t.task}, status: ${status_to_str(t.status)}, edited: ${edited_to_str(decode_edited(t.edited_val))}")) + # Example: filter rows by status (decode a single column) tasks_in_progress = Sqlite.query_many!( { path: db_path, query: "SELECT id, task, status FROM todos WHERE status = :status;", bindings: [{ name: ":status", value: encode_status(InProgress) }], - rows: Sqlite.str("task") + rows: Sqlite.str("task"), }, )? - Stdout.line!("\nIn-progress Todos:")? - - List.for_each_try!( - tasks_in_progress, - |task_description| - Stdout.line!("\tIn-progress tasks: ${task_description}"), - )? + _h2 = Stdout.line!("") + _h3 = Stdout.line!("In-progress Todos:") + List.for_each!(tasks_in_progress, |task| print_line!(" In-progress task: ${task}")) # Example: insert a row - - Sqlite.execute!({ - path: db_path, - query: "INSERT INTO todos (task, status, edited) VALUES (:task, :status, :edited);", - bindings: [ - { name: ":task", value: String("Make sql example.") }, - { name: ":status", value: encode_status(InProgress) }, - { name: ":edited", value: encode_edited(NotEdited) }, - ], - })? + Sqlite.execute!( + { + path: db_path, + query: "INSERT INTO todos (task, status, edited) VALUES (:task, :status, :edited);", + bindings: [ + { name: ":task", value: String("Make sql example.") }, + { name: ":status", value: encode_status(InProgress) }, + { name: ":edited", value: encode_edited(NotEdited) }, + ], + }, + )? # Example: insert multiple rows from a Roc list - - todos_list : List ({task : Str, status : TodoStatus, edited : EditedValue}) todos_list = [ { task: "Insert Roc list 1", status: Todo, edited: NotEdited }, { task: "Insert Roc list 2", status: Todo, edited: NotEdited }, @@ -89,136 +75,171 @@ main! = |_args| ] values_str = - todos_list - |> List.map_with_index( - |_, indx| - indx_str = Num.to_str(indx) - "(:task${indx_str}, :status${indx_str}, :edited${indx_str})", + Str.join_with( + List.map_with_index(todos_list, |_t, indx| { + i = U64.to_str(indx) + "(:task${i}, :status${i}, :edited${i})" + }), + ", ", ) - |> Str.join_with(", ") + + binding_groups = + List.map_with_index(todos_list, |t, indx| { + i = U64.to_str(indx) + [ + { name: ":task${i}", value: String(t.task) }, + { name: ":status${i}", value: encode_status(t.status) }, + { name: ":edited${i}", value: encode_edited(t.edited) }, + ] + }) all_bindings = - todos_list - |> List.map_with_index( - |{ task, status, edited }, indx| - indx_str = Num.to_str(indx) - [ - { name: ":task${indx_str}", value: String(task) }, - { name: ":status${indx_str}", value: encode_status(status) }, - { name: ":edited${indx_str}", value: encode_edited(edited) }, - ], - ) - |> List.join + Iter.fold(List.iter(binding_groups), [], |acc, group| List.concat(acc, group)) - Sqlite.execute!({ - path: db_path, - query: "INSERT INTO todos (task, status, edited) VALUES ${values_str};", - bindings: all_bindings, - })? + Sqlite.execute!( + { + path: db_path, + query: "INSERT INTO todos (task, status, edited) VALUES ${values_str};", + bindings: all_bindings, + }, + )? # Example: update a row - - Sqlite.execute!({ - path: db_path, - query: "UPDATE todos SET status = :status WHERE task = :task;", - bindings: [ - { name: ":task", value: String("Make sql example.") }, - { name: ":status", value: encode_status(Completed) }, - ], - })? + Sqlite.execute!( + { + path: db_path, + query: "UPDATE todos SET status = :status WHERE task = :task;", + bindings: [ + { name: ":task", value: String("Make sql example.") }, + { name: ":status", value: encode_status(Completed) }, + ], + }, + )? # Example: delete a row + Sqlite.execute!( + { + path: db_path, + query: "DELETE FROM todos WHERE task = :task;", + bindings: [{ name: ":task", value: String("Make sql example.") }], + }, + )? - Sqlite.execute!({ - path: db_path, - query: "DELETE FROM todos WHERE task = :task;", - bindings: [ - { name: ":task", value: String("Make sql example.") }, - ], - })? - - # Example: delete all rows where ID is greater than 3 - - Sqlite.execute!({ - path: db_path, - query: "DELETE FROM todos WHERE id > :id;", - bindings: [ - { name: ":id", value: Integer(3) }, - ], - })? + # Example: delete all rows where ID is greater than 3 (cleanup so this example is repeatable) + Sqlite.execute!( + { + path: db_path, + query: "DELETE FROM todos WHERE id > :id;", + bindings: [{ name: ":id", value: Integer(3) }], + }, + )? # Example: count the number of rows + count = Sqlite.query!( + { + path: db_path, + query: "SELECT COUNT(*) as \"count\" FROM todos;", + bindings: [], + row: Sqlite.u64("count"), + }, + )? - count = Sqlite.query!({ - path: db_path, - query: "SELECT COUNT(*) as \"count\" FROM todos;", - bindings: [], - row: Sqlite.u64("count"), - })? - - expect count == 3 + _hc = Stdout.line!("") + _hcount = Stdout.line!("Row count: ${U64.to_str(count)}") # Example: prepared statements - # Note: This leads to better performance if you are executing the same prepared statement multiple times. - - prepared_query = Sqlite.prepare!({ - path : db_path, - query : "SELECT * FROM todos ORDER BY LENGTH(task);", # sort by the length of the task description - })? - - todos_sorted = Sqlite.query_many_prepared!({ - stmt: prepared_query, - bindings: [], - rows: { Sqlite.decode_record <- - task: Sqlite.str("task"), - status: Sqlite.str("status") |> Sqlite.map_value_result(decode_status), + # Note: This is faster if you execute the same prepared statement many times. + prepared_query = Sqlite.prepare!( + { + path: db_path, + # sort by the length of the task description + query: "SELECT * FROM todos ORDER BY LENGTH(task);", }, - })? - - Stdout.line!("\nTodos sorted by length of task description:")? + )? - List.for_each_try!( - todos_sorted, - |{ task, status }| - Stdout.line!("\t task: ${task}, status: ${Inspect.to_str(status)}"), + todos_sorted = Sqlite.query_many_prepared!( + { + stmt: prepared_query, + bindings: [], + rows: decode_task_status, + }, )? + _h4 = Stdout.line!("") + _h5 = Stdout.line!("Todos sorted by length of task description:") + List.for_each!(todos_sorted, |t| print_line!(" task: ${t.task}, status: ${status_to_str(t.status)}")) + Ok({}) +} + +print_line! = |s| + match Stdout.line!(s) { + _ => {} + } + +# Decode every column of the todos table. The nullable `edited` column is returned +# raw (`[NotNull(I64), Null]`) and interpreted by `decode_edited` at the call site: +# decoding both `status` (via `?`) and `edited` inside this nested decoder lambda +# currently panics the type checker, so we keep only one interpreting `?` here. +decode_full_todo = |cols| + |stmt| { + id = Sqlite.i64("id")(cols)(stmt)? + task = Sqlite.str("task")(cols)(stmt)? + status_str = Sqlite.str("status")(cols)(stmt)? + status = decode_status(status_str)? + edited_val = Sqlite.nullable_i64("edited")(cols)(stmt)? + Ok({ id, task, status, edited_val }) + } + +# Decode just the task and status columns. +decode_task_status = |cols| + |stmt| { + task = Sqlite.str("task")(cols)(stmt)? + status_str = Sqlite.str("status")(cols)(stmt)? + status = decode_status(status_str)? + Ok({ task, status }) + } TodoStatus : [Todo, Completed, InProgress] -decode_status : Str -> Result TodoStatus _ decode_status = |status_str| - when status_str is - "todo" -> Ok(Todo) - "completed" -> Ok(Completed) - "in-progress" -> Ok(InProgress) - _ -> Err(ParseError("Unknown status str: ${status_str}")) + match status_str { + "todo" => Ok(Todo) + "completed" => Ok(Completed) + "in-progress" => Ok(InProgress) + _ => Err(ParseError("Unknown status str: ${status_str}")) + } status_to_str : TodoStatus -> Str status_to_str = |status| - when status is - Todo -> "todo" - Completed -> "completed" - InProgress -> "in-progress" + match status { + Todo => "todo" + Completed => "completed" + InProgress => "in-progress" + } - -encode_status : TodoStatus -> [String Str] -encode_status = |status| - String(status_to_str(status)) +encode_status = |status| String(status_to_str(status)) -EditedValue : [Edited, NotEdited, Null] +EditedValue : [Edited, NotEdited, Unknown] -decode_edited : [NotNull I64, Null] -> EditedValue decode_edited = |edited_val| - when edited_val is - NotNull 1 -> Edited - NotNull 0 -> NotEdited - _ -> Null + match edited_val { + NotNull(1) => Edited + NotNull(0) => NotEdited + _ => Unknown + } + +edited_to_str : EditedValue -> Str +edited_to_str = |edited| + match edited { + Edited => "edited" + NotEdited => "not-edited" + Unknown => "unknown" + } -encode_edited : EditedValue -> [Integer I64, Null] encode_edited = |edited| - when edited is - Edited -> Integer(1) - NotEdited -> Integer(0) - Null -> Null + match edited { + Edited => Integer(1) + NotEdited => Integer(0) + Unknown => Null + } diff --git a/examples/stdin-basic.roc b/examples/stdin-basic.roc index 2c5c307d..1544abf9 100644 --- a/examples/stdin-basic.roc +++ b/examples/stdin-basic.roc @@ -2,22 +2,20 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdin import pf.Stdout -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder - -# Reading text from stdin. -# If you want to read Stdin from a pipe, check out examples/stdin-pipe.roc - -main! : List Arg => Result {} _ -main! = |_args| - - Stdout.line!("What's your first name?")? - - first = Stdin.line!({})? - - Stdout.line!("What's your last name?")? - - last = Stdin.line!({})? - - Stdout.line!("Hi, ${first} ${last}! 👋") +main! = |_args| { + match Stdout.line!("What's your first name?") { _ => {} } + first = match Stdin.line!({}) { + Ok(line) => line + Err(_) => "" + } + + match Stdout.line!("What's your last name?") { _ => {} } + last = match Stdin.line!({}) { + Ok(line) => line + Err(_) => "" + } + + match Stdout.line!("Hi, ${first} ${last}! \u(1F44B)") { _ => {} } + Ok({}) +} diff --git a/examples/stdin-pipe.roc b/examples/stdin-pipe.roc index 80d18140..e4315c77 100644 --- a/examples/stdin-pipe.roc +++ b/examples/stdin-pipe.roc @@ -2,18 +2,16 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdin import pf.Stdout -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder # Reading piped text from stdin, for example: `echo "hey" | roc ./examples/stdin-pipe.roc` -main! : List Arg => Result {} _ -main! = |_args| - +main! = |_args| { # Data is only sent with Stdin.line! if the user presses Enter, # so you'll need to use read_to_end! to read data that was piped in without a newline. piped_in = Stdin.read_to_end!({})? piped_in_str = Str.from_utf8(piped_in)? Stdout.line!("This is what you piped in: \"${piped_in_str}\"") +} diff --git a/examples/tcp-client.roc b/examples/tcp-client.roc index 6276cd84..8cbd1fdf 100644 --- a/examples/tcp-client.roc +++ b/examples/tcp-client.roc @@ -4,75 +4,78 @@ import pf.Tcp import pf.Stdout import pf.Stdin import pf.Stderr -import pf.Arg exposing [Arg] - -# To run this example: check the README.md in this folder # Simple TCP client in Roc. -# Connects to a server on localhost:8085, reads user input from stdin, -# sends it to the server, and prints the server's response. - -main! : List Arg => Result {} _ +# +# Connects to a server on localhost:8085, reads user input from stdin, sends it +# to the server, and prints the server's response — looping until end-of-input. +# +# To try it interactively, start an echo server in another terminal first: +# +# $ ncat -e $(which cat) -l 8085 +# +# then run this example. +main! : List(Str) => Try({}, [Exit(I32), ..]) main! = |_args| - - tcp_stream = Tcp.connect!("127.0.0.1", 8085)? - - Stdout.line!("Connected!")? - - loop!( - {}, - |_| Result.map_ok(tick!(tcp_stream), Step), + match Tcp.connect!("127.0.0.1", 8085) { + Ok(stream) => { + _ = Stdout.line!("Connected!") + run!(stream) + } + Err(connect_err) => report_connect_err!(connect_err) + } + +## Read a line from stdin, send it to the server, print the response, repeat. +run! : Tcp.Stream => Try({}, [Exit(I32), ..]) +run! = |stream| { + _ = Stdout.write!("> ") + match Stdin.line!({}) { + # No more input — exit cleanly. + Err(EndOfFile) => Ok({}) + Err(StdinErr(_)) => Ok({}) + Ok(out_msg) => + match Tcp.write_utf8!(stream, "${out_msg}\n") { + Err(TcpWriteErr(err)) => report_stream_err!("writing", err) + Ok({}) => + match Tcp.read_line!(stream) { + Err(read_err) => report_read_err!(read_err) + Ok(in_msg) => { + _ = Stdout.line!("< ${in_msg}") + run!(stream) + } + } + } + } +} + +report_connect_err! : Tcp.ConnectErr => Try({}, [Exit(I32), ..]) +report_connect_err! = |err| { + err_str = Tcp.connect_err_to_str(err) + _ = Stderr.line!( + \\Failed to connect: ${err_str} + \\ + \\If you don't have anything listening on port 8085, run: + \\ $ nc -l 8085 + \\ + \\If you want an echo server you can run: + \\ $ ncat -e $(which cat) -l 8085 ) - |> Result.on_err!(handle_err!) - -## Read from stdin, send to the server, and print the response. -tick! : Tcp.Stream => Result {} _ -tick! = |tcp_stream| - Stdout.write!("> ")? - - out_msg = Stdin.line!({})? - - Tcp.write_utf8!(tcp_stream, "${out_msg}\n")? - - in_msg = Tcp.read_line!(tcp_stream)? - - Stdout.line!("< ${in_msg}") - - -loop! : state, (state => Result [Step state, Done done] err) => Result done err -loop! = |state, fn!| - when fn!(state) is - Err(err) -> Err(err) - Ok(Done(done)) -> Ok(done) - Ok(Step(next)) -> loop!(next, fn!) - - -handle_err! : []_ => Result {} _ -handle_err! = |error| - when error is - TcpConnectErr(err) -> - err_str = Tcp.connect_err_to_str(err) - Stderr.line!( - """ - Failed to connect: ${err_str} - - If you don't have anything listening on port 8085, run: - \$ nc -l 8085 - - If you want an echo server you can run: - $ ncat -e \$(which cat) -l 8085 - """, - ) - - TcpReadBadUtf8(_) -> - Stderr.line!("Received invalid UTF-8 data") - - TcpReadErr(err) -> - err_str = Tcp.stream_err_to_str(err) - Stderr.line!("Error while reading: ${err_str}") - - TcpWriteErr(err) -> - err_str = Tcp.stream_err_to_str(err) - Stderr.line!("Error while writing: ${err_str}") - - other_err -> Stderr.line!("Unhandled error: ${Inspect.to_str(other_err)}") + Ok({}) +} + +report_read_err! : [TcpReadErr(Tcp.StreamErr), TcpReadBadUtf8(_)] => Try({}, [Exit(I32), ..]) +report_read_err! = |err| + match err { + TcpReadErr(stream_err) => report_stream_err!("reading", stream_err) + TcpReadBadUtf8(_) => { + _ = Stderr.line!("Received invalid UTF-8 data") + Ok({}) + } + } + +report_stream_err! : Str, Tcp.StreamErr => Try({}, [Exit(I32), ..]) +report_stream_err! = |action, err| { + err_str = Tcp.stream_err_to_str(err) + _ = Stderr.line!("Error while ${action}: ${err_str}") + Ok({}) +} diff --git a/examples/temp-dir.roc b/examples/temp-dir.roc index fc4e7c05..2f0eb76a 100644 --- a/examples/temp-dir.roc +++ b/examples/temp-dir.roc @@ -2,8 +2,6 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.Env -import pf.Path -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder @@ -12,9 +10,8 @@ import pf.Arg exposing [Arg] # !! this requires the flag `--linker=legacy`: # for example: `roc build examples/temp-dir.roc --linker=legacy` -main! : List Arg => Result {} _ -main! = |_args| - - temp_dir_path_str = Path.display(Env.temp_dir!({})) +main! = |_args| { + temp_dir_path_str = Env.temp_dir!({}) Stdout.line!("The temp dir path is ${temp_dir_path_str}") +} diff --git a/examples/terminal-app-snake.roc b/examples/terminal-app-snake.roc deleted file mode 100644 index 2a593553..00000000 --- a/examples/terminal-app-snake.roc +++ /dev/null @@ -1,230 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Stdin -import pf.Stdout -import pf.Tty -import pf.Arg exposing [Arg] - -# To run this example: check the README.md in this folder - -# If you want to make a full screen terminal app, you probably want to switch the terminal to [raw mode](https://en.wikipedia.org/wiki/Terminal_mode). -# Here we demonstrate `Tty.enable_raw_mode!` and `Tty.disable_raw_mode!` with a simple snake game. - -Position : { x : I64, y : I64 } - -GameState : { - snake_lst : NonEmptyList, - food_pos : Position, - direction : [Up, Down, Left, Right], - game_over : Bool, -} - -# The snake list should never be empty, so we use a non-empty list. -# Typically we'd use head and tail, but this would be confusing with the snake's head and tail later on :) -NonEmptyList : { first : Position, rest : List Position } - -initial_state = { - snake_lst: { first: { x: 10, y: 10 }, rest: [{ x: 9, y: 10 }, { x: 8, y: 10 }] }, - food_pos: { x: 15, y: 15 }, - direction: Right, - game_over: Bool.false, -} - -# Keep this above 15 for the initial food_pos -grid_size = 20 - -init_snake_len = len(initial_state.snake_lst) - -main! : List Arg => Result {} _ -main! = |_args| - Tty.enable_raw_mode!({}) - - game_loop!(initial_state)? - - Tty.disable_raw_mode!({}) - Stdout.line!("\n--- Game Over ---") - -game_loop! : GameState => Result {} _ -game_loop! = |state| - if state.game_over then - Ok({}) - else - draw_game!(state)? - - # Check keyboard input - input_bytes = Stdin.bytes!({})? - - partial_new_state = - when input_bytes is - ['w'] -> { state & direction: Up } - ['s'] -> { state & direction: Down } - ['a'] -> { state & direction: Left } - ['d'] -> { state & direction: Right } - ['q'] -> { state & game_over: Bool.true } - _ -> state - - new_state = update_game(partial_new_state) - game_loop!(new_state) - -update_game : GameState -> GameState -update_game = |state| - if state.game_over then - state - else - snake_head_pos = state.snake_lst.first - new_head_pos = move_head(snake_head_pos, state.direction) - - new_state = - # Check wall collision - if new_head_pos.x < 0 or new_head_pos.x >= grid_size or new_head_pos.y < 0 or new_head_pos.y >= grid_size then - { state & game_over: Bool.true } - - # Check self collision - else if contains(state.snake_lst, new_head_pos) then - { state & game_over: Bool.true } - - # Check food collision - else if new_head_pos == state.food_pos then - new_snake_lst = prepend(state.snake_lst, new_head_pos) - - new_food_pos = { x: (new_head_pos.x + 3) % grid_size, y: (new_head_pos.y + 3) % grid_size } - - { state & snake_lst: new_snake_lst, food_pos: new_food_pos } - - # No collision; move the snake - else - new_snake_lst = - prepend(state.snake_lst, new_head_pos) - |> |snake_lst| { first: snake_lst.first, rest: List.drop_last(snake_lst.rest, 1) } - - { state & snake_lst: new_snake_lst } - - new_state - -move_head : Position, [Down, Left, Right, Up] -> Position -move_head = |head, direction| - when direction is - Up -> { head & y: head.y - 1 } - Down -> { head & y: head.y + 1 } - Left -> { head & x: head.x - 1 } - Right -> { head & x: head.x + 1 } - -draw_game! : GameState => Result {} _ -draw_game! = |state| - clear_screen!({})? - - Stdout.line!("\nControls: W A S D to move, Q to quit\n\r")? - - # \r to fix indentation because we're in raw mode - Stdout.line!("Score: ${Num.to_str(len(state.snake_lst) - init_snake_len)}\r")? - - rendered_game_str = draw_game_pure(state) - - Stdout.line!("${rendered_game_str}\r") - -draw_game_pure : GameState -> Str -draw_game_pure = |state| - List.range({ start: At 0, end: Before grid_size }) - |> List.map( - |yy| - line = - List.range({ start: At 0, end: Before grid_size }) - |> List.map( - |xx| - pos = { x: xx, y: yy } - if contains(state.snake_lst, pos) then - if pos == state.snake_lst.first then - "O" # Snake head - else - "o" # Snake body - else if pos == state.food_pos then - "*" # food_pos - else - ".", # Empty space - ) - |> Str.join_with("") - - line, - ) - |> Str.join_with("\r\n") - -clear_screen! = |{}| - Stdout.write!("\u(001b)[2J\u(001b)[H") # ANSI escape codes to clear screen - -# NonEmptyList helpers - -contains : NonEmptyList, Position -> Bool -contains = |list, pos| - list.first == pos or List.contains(list.rest, pos) - -prepend : NonEmptyList, Position -> NonEmptyList -prepend = |list, pos| - { first: pos, rest: List.prepend(list.rest, list.first) } - -len : NonEmptyList -> U64 -len = |list| - 1 + List.len(list.rest) - -# Tests - -expect - grid_size == 20 # The tests below assume a grid size of 20 - -expect - initial_grid = draw_game_pure(initial_state) - expected_grid = - """ - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ........ooO.........\r - ....................\r - ....................\r - ....................\r - ....................\r - ...............*....\r - ....................\r - ....................\r - ....................\r - .................... - """ - - initial_grid == expected_grid - -# Test moving down -expect - new_state = update_game({ initial_state & direction: Down }) - new_grid = draw_game_pure(new_state) - - expected_grid = - """ - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - .........oo.........\r - ..........O.........\r - ....................\r - ....................\r - ....................\r - ...............*....\r - ....................\r - ....................\r - ....................\r - .................... - """ - - new_grid == expected_grid diff --git a/examples/terminal-app-snake.todoroc b/examples/terminal-app-snake.todoroc new file mode 100644 index 00000000..2aaea726 --- /dev/null +++ b/examples/terminal-app-snake.todoroc @@ -0,0 +1,234 @@ +app [main!] { pf: platform "../platform/main.roc" } + +# TODO(roc-lang/roc#9749): skipped until the Roc postcheck crash is fixed. +# This example checks successfully when renamed to .roc, but roc build panics +# on List.contains over a list of record values in the renderer. + +import pf.Stdin +import pf.Stdout +import pf.Tty + +# If you want to make a full screen terminal app, you probably want to switch +# the terminal to raw mode. Here we demonstrate Tty.enable_raw_mode! and +# Tty.disable_raw_mode! with a small snake game. + +Position : { x : I64, y : I64 } + +Snake : { first : Position, rest : List(Position) } + +Direction : { dx : I64, dy : I64 } + +GameState : { + snake : Snake, + food : Position, + direction : Direction, + game_over : Bool, +} + +initial_state : GameState +initial_state = { + snake: { first: { x: 10, y: 10 }, rest: [{ x: 9, y: 10 }, { x: 8, y: 10 }] }, + food: { x: 15, y: 15 }, + direction: right, + game_over: Bool.False, +} + +grid_size : I64 +grid_size = 20 + +up : Direction +up = { dx: 0, dy: -1 } + +down : Direction +down = { dx: 0, dy: 1 } + +left : Direction +left = { dx: -1, dy: 0 } + +right : Direction +right = { dx: 1, dy: 0 } + +init_snake_len : U64 +init_snake_len = snake_len(initial_state.snake) + +main! : List(Str) => Try({}, [Exit(I32)]) +main! = |_args| { + Tty.enable_raw_mode!() + game_loop!(initial_state)? + Tty.disable_raw_mode!() + Stdout.line!("\n--- Game Over ---").map_err(|_| Exit(1)) +} + +game_loop! : GameState => Try({}, [Exit(I32)]) +game_loop! = |state| { + if state.game_over { + Ok({}) + } else { + draw_game!(state)? + + input_bytes = Stdin.bytes!({}).map_err(|_| Exit(1))? + new_state = update_game(apply_input(state, input_bytes)) + + game_loop!(new_state) + } +} + +apply_input : GameState, List(U8) -> GameState +apply_input = |state, input_bytes| + match input_bytes.get(0) { + Ok(byte) => + if byte == 119 { + { ..state, direction: up } + } else if byte == 115 { + { ..state, direction: down } + } else if byte == 97 { + { ..state, direction: left } + } else if byte == 100 { + { ..state, direction: right } + } else if byte == 113 { + { ..state, game_over: Bool.True } + } else { + state + } + + Err(_) => state + } + +update_game : GameState -> GameState +update_game = |state| { + if state.game_over { + state + } else { + new_head = move_head(state.snake.first, state.direction) + + if hit_wall(new_head) or snake_contains(state.snake, new_head) { + { ..state, game_over: Bool.True } + } else if new_head == state.food { + new_snake = snake_prepend(state.snake, new_head) + new_food = { x: (new_head.x + 3) % grid_size, y: (new_head.y + 3) % grid_size } + + { ..state, snake: new_snake, food: new_food } + } else { + grown = snake_prepend(state.snake, new_head) + moved = { first: grown.first, rest: List.drop_last(grown.rest, 1) } + + { ..state, snake: moved } + } + } +} + +hit_wall : Position -> Bool +hit_wall = |pos| + pos.x < 0 or pos.x >= grid_size or pos.y < 0 or pos.y >= grid_size + +move_head : Position, Direction -> Position +move_head = |head, direction| + { x: head.x + direction.dx, y: head.y + direction.dy } + +draw_game! : GameState => Try({}, [Exit(I32)]) +draw_game! = |state| { + clear_screen!({})? + + Stdout.line!("\nControls: W A S D to move, Q to quit\n\r").map_err(|_| Exit(1))? + Stdout.line!("Score: ${(snake_len(state.snake) - init_snake_len).to_str()}\r").map_err(|_| Exit(1))? + + rendered_game_str = draw_game_pure(state) + Stdout.line!("${rendered_game_str}\r").map_err(|_| Exit(1)) +} + +draw_game_pure : GameState -> Str +draw_game_pure = |state| + draw_rows(state, 0, []) + +draw_rows : GameState, I64, List(Str) -> Str +draw_rows = |state, yy, rows| { + if yy >= grid_size { + Str.join_with(rows, "\r\n") + } else { + draw_rows(state, yy + 1, rows.append(draw_row(state, yy))) + } +} + +draw_row : GameState, I64 -> Str +draw_row = |state, yy| + draw_cells(state, yy, 0, []) + +draw_cells : GameState, I64, I64, List(Str) -> Str +draw_cells = |state, yy, xx, cells| { + if xx >= grid_size { + Str.join_with(cells, "") + } else { + pos = { x: xx, y: yy } + cell = + if pos == state.snake.first { + "O" + } else if List.contains(state.snake.rest, pos) { + "o" + } else if pos == state.food { + "*" + } else { + "." + } + + draw_cells(state, yy, xx + 1, cells.append(cell)) + } +} + +clear_screen! : {} => Try({}, [Exit(I32)]) +clear_screen! = |_| Stdout.write!("\u(001b)[2J\u(001b)[H").map_err(|_| Exit(1)) + +snake_contains : Snake, Position -> Bool +snake_contains = |snake, pos| + snake.first == pos or List.contains(snake.rest, pos) + +snake_prepend : Snake, Position -> Snake +snake_prepend = |snake, pos| + { first: pos, rest: List.prepend(snake.rest, snake.first) } + +snake_len : Snake -> U64 +snake_len = |snake| + 1 + List.len(snake.rest) + +initial_grid = + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\........ooO......... + \\.................... + \\.................... + \\.................... + \\.................... + \\...............*.... + \\.................... + \\.................... + \\.................... + \\.................... + +moved_down_grid = + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.........oo......... + \\..........O......... + \\.................... + \\.................... + \\.................... + \\...............*.... + \\.................... + \\.................... + \\.................... + \\.................... diff --git a/examples/time.roc b/examples/time.roc index 07c330a0..ea8adcc2 100644 --- a/examples/time.roc +++ b/examples/time.roc @@ -3,14 +3,10 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.Utc import pf.Sleep -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder +# Demo Utc and Sleep functions -# Demo Utc and sleep functions - -main! : List Arg => Result {} _ -main! = |_args| +main! = |_args| { start = Utc.now!({}) # 1000 ms = 1 second @@ -18,6 +14,10 @@ main! = |_args| finish = Utc.now!({}) - duration = Num.to_str(Utc.delta_as_nanos(start, finish)) + duration_ms = Utc.delta_as_millis(finish, start) + duration_nanos = Utc.delta_as_nanos(finish, start) + + _r = Stdout.line!("Completed in ${duration_ms.to_str()} ms (${duration_nanos.to_str()} ns)") - Stdout.line!("Completed in ${duration} ns") + Ok({}) +} diff --git a/examples/tty.roc b/examples/tty.roc new file mode 100644 index 00000000..8c30b1b9 --- /dev/null +++ b/examples/tty.roc @@ -0,0 +1,17 @@ +app [main!] { pf: platform "../platform/main.roc" } + +import pf.Stdout +import pf.Tty + +## Raw mode allows you to change the behaviour of the terminal. +## This is useful for running an app like vim or a game in the terminal. + +main! = |_args| { + match Stdout.line!("Tty: enabling raw mode") { _ => {} } + Tty.enable_raw_mode!() + + match Stdout.line!("Tty: disabling raw mode") { _ => {} } + Tty.disable_raw_mode!() + + Ok({}) +} diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 377e0420..00000000 --- a/flake.lock +++ /dev/null @@ -1,177 +0,0 @@ -{ - "nodes": { - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1722403750, - "narHash": "sha256-tRmn6UiFAPX0m9G1AVcEPjWEOc9BtGsxGcs7Bz3MpsM=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "184957277e885c06a505db112b35dfbec7c60494", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "rev": "184957277e885c06a505db112b35dfbec7c60494", - "type": "github" - } - }, - "roc": { - "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - }, - "locked": { - "lastModified": 1756241842, - "narHash": "sha256-3c+8PaWe+MFOuj1DxYiI1eOlHru6FxH6ByzboYKr29I=", - "owner": "roc-lang", - "repo": "roc", - "rev": "2a924d2a83244460b5174e5459f5b62a61013992", - "type": "github" - }, - "original": { - "owner": "roc-lang", - "repo": "roc", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": [ - "roc", - "nixpkgs" - ], - "roc": "roc", - "rust-overlay": "rust-overlay_2" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "roc", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1736303309, - "narHash": "sha256-IKrk7RL+Q/2NC6+Ql6dwwCNZI6T6JH2grTdJaVWHF0A=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "a0b81d4fa349d9af1765b0f0b4a899c13776f706", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "rust-overlay_2": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756262090, - "narHash": "sha256-PQHSup4d0cVXxJ7mlHrrxBx1WVrmudKiNQgnNl5xRas=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "df7ea78aded79f195a92fc5423de96af2b8a85d1", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 2005ebfd..00000000 --- a/flake.nix +++ /dev/null @@ -1,97 +0,0 @@ -{ - description = "Basic cli devShell flake"; - - inputs = { - roc.url = "github:roc-lang/roc"; - - nixpkgs.follows = "roc/nixpkgs"; - - # rust from nixpkgs has some libc problems, this is patched in the rust-overlay - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - # to easily make configs for multiple architectures - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, roc, rust-overlay, flake-utils }: - let supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; - in flake-utils.lib.eachSystem supportedSystems (system: - let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { inherit system overlays; }; - - rocPkgs = roc.packages.${system}; - llvmPkgs = pkgs.llvmPackages_16; - - # get current working directory - cwd = builtins.toString ./.; - rust = - pkgs.rust-bin.fromRustupToolchainFile "${toString ./rust-toolchain.toml}"; - - shellFunctions = '' - buildcmd() { - bash jump-start.sh && roc ./build.roc -- --roc roc - } - export -f buildcmd - - testcmd() { - export EXAMPLES_DIR=./examples/ && ./ci/all_tests.sh - } - export -f testcmd - ''; - - linuxInputs = with pkgs; - lib.optionals stdenv.isLinux [ - valgrind - ]; - - darwinInputs = with pkgs; - lib.optionals stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - ]); - - sharedInputs = (with pkgs; [ - sqlite - jq - rust - llvmPkgs.clang - llvmPkgs.lldb # for debugging - expect - nmap - simple-http-server - rocPkgs.cli - ripgrep # for ci/check_all_exposed_funs_tested.roc - ]); - in { - - devShell = pkgs.mkShell { - buildInputs = sharedInputs ++ darwinInputs ++ linuxInputs; - - # nix does not store libs in /usr/lib or /lib - # for libgcc_s.so.1 - NIX_LIBGCC_S_PATH = - if pkgs.stdenv.isLinux then "${pkgs.stdenv.cc.cc.lib}/lib" else ""; - # for crti.o, crtn.o, and Scrt1.o - NIX_GLIBC_PATH = - if pkgs.stdenv.isLinux then "${pkgs.glibc.out}/lib" else ""; - - shellHook = '' - export ROC=roc - - ${shellFunctions} - - echo "Some convenient commands:" - echo "${shellFunctions}" | grep -E '^\s*[a-zA-Z_][a-zA-Z0-9_]*\(\)' | sed 's/().*//' | sed 's/^[[:space:]]*/ /' | while read func; do - body=$(echo "${shellFunctions}" | sed -n "/''${func}()/,/^[[:space:]]*}/p" | sed '1d;$d' | tr '\n' ';' | sed 's/;$//' | sed 's/[[:space:]]*$//') - echo " $func = $body" - done - echo "" - ''; - }; - - formatter = pkgs.nixpkgs-fmt; - }); -} diff --git a/jump-start.sh b/jump-start.sh deleted file mode 100755 index 109f799f..00000000 --- a/jump-start.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash - -## This script is only needed in the event of a breaking change in the -## Roc compiler that prevents build.roc from running. -## This script builds a local prebuilt binary for the native target, -## so that the build.roc script can be run. -## -## To use this, change the build.roc script to use the platform locally.. - -# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ -set -exo pipefail - -if [ -z "${ROC}" ]; then - echo "Warning: ROC environment variable is not set... I'll try with just 'roc'." - - ROC="roc" -fi - -$ROC build --lib ./platform/libapp.roc - -cargo build --release - -if [ -n "$CARGO_BUILD_TARGET" ]; then - cp target/$CARGO_BUILD_TARGET/release/libhost.a ./platform/libhost.a -else - cp target/release/libhost.a ./platform/libhost.a -fi - -$ROC build --linker=legacy build.roc - -./build diff --git a/platform/Arg.roc b/platform/Arg.roc deleted file mode 100644 index 8c4db92a..00000000 --- a/platform/Arg.roc +++ /dev/null @@ -1,55 +0,0 @@ -module [ - Arg, - display, - to_os_raw, - from_os_raw, -] - -## An OS-aware (see below) representation of a command-line argument. -## -## Though we tend to think of args as Unicode strings, **most operating systems -## represent command-line arguments as lists of bytes** that aren't necessarily -## UTF-8 encoded. Windows doesn't even use bytes, but U16s. -## -## Most of the time, you will pass these to packages and they will handle the -## encoding for you, but for quick-and-dirty code you can use [display] to -## convert these to [Str] in a lossy way. -Arg := [Unix (List U8), Windows (List U16)] - implements [Eq, Inspect { to_inspector: arg_inspector }] - -arg_inspector : Arg -> Inspector f where f implements InspectFormatter -arg_inspector = |arg| Inspect.str(display(arg)) - -test_hello : Arg -test_hello = Arg.from_os_raw(Unix([72, 101, 108, 108, 111])) - -expect Arg.display(test_hello) == "Hello" -expect Inspect.to_str(test_hello) == "\"Hello\"" - -## Unwrap an [Arg] into a raw, OS-aware numeric list. -## -## This is a good way to pass [Arg]s to Roc packages. -to_os_raw : Arg -> [Unix (List U8), Windows (List U16)] -to_os_raw = |@Arg(inner)| inner - -## Wrap a raw, OS-aware numeric list into an [Arg]. -from_os_raw : [Unix (List U8), Windows (List U16)] -> Arg -from_os_raw = @Arg - -## Convert an Arg to a `Str` for display purposes. -## -## NB: this will currently crash if there is invalid utf8 bytes, in future this will be lossy and replace any invalid bytes with the [Unicode Replacement Character U+FFFD �](https://en.wikipedia.org/wiki/Specials_(Unicode_block)) -display : Arg -> Str -display = |@Arg(inner)| - when inner is - Unix(bytes) -> - # TODO replace with Str.from_utf8_lossy : List U8 -> Str - # see https://github.com/roc-lang/roc/issues/7390 - when Str.from_utf8(bytes) is - Ok(str) -> str - Err(_) -> crash("tried to display Arg containing invalid utf-8") - - Windows(_) -> - # TODO replace with Str.from_utf16_lossy : List U16 -> Str - # see https://github.com/roc-lang/roc/issues/7390 - crash("display for utf-16 Arg not yet supported") diff --git a/platform/Cmd.roc b/platform/Cmd.roc index fa152f86..6ecaecf1 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -1,238 +1,293 @@ -module [ - Cmd, - new, - arg, - args, - env, - envs, - clear_envs, - exec_output!, - exec_output_bytes!, - exec!, - exec_cmd!, - exec_exit_code!, -] - -import InternalCmd exposing [to_str] -import InternalIOErr exposing [IOErr] -import Host - -## Simplest way to execute a command while inheriting stdin, stdout and stderr from parent. -## If you want to capture the output, use [exec_output!] instead. -## ``` -## # Call echo to print "hello world" -## Cmd.exec!("echo", ["hello world"])? -## ``` -exec! : Str, List Str => Result {} [ExecFailed { command : Str, exit_code : I32 }, FailedToGetExitCode { command : Str, err : IOErr }] -exec! = |cmd_name, arguments| - exit_code = - new(cmd_name) - |> args(arguments) - |> exec_exit_code!()? - - if exit_code == 0i32 then - Ok({}) - else - command = "${cmd_name} ${Str.join_with(arguments, " ")}" - Err(ExecFailed({ command, exit_code })) - -## Execute a Cmd while inheriting stdin, stdout and stderr from parent. -## 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. -## ``` -## # Execute `cargo build` with env var. -## Cmd.new("cargo") -## |> Cmd.arg("build") -## |> Cmd.env("RUST_BACKTRACE", "1") -## |> Cmd.exec_cmd!()? -## ``` -exec_cmd! : Cmd => Result {} [ExecCmdFailed { command : Str, exit_code : I32 }, FailedToGetExitCode { command : Str, err : IOErr }] -exec_cmd! = |@Cmd(cmd)| - exit_code = - exec_exit_code!(@Cmd(cmd))? - - if exit_code == 0i32 then - Ok({}) - else - Err(ExecCmdFailed({ command: to_str(cmd), exit_code })) - -## Execute command and capture stdout and stderr. -## -## > Stdin is not inherited from the parent and any attempt by the child process -## > to read from the stdin stream will result in the stream immediately closing. -## -## 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. -## -## ``` -## cmd_output = -## Cmd.new("echo") -## |> Cmd.args(["Hi"]) -## |> Cmd.exec_output!()? -## -## Stdout.line!("Echo output: ${cmd_output.stdout_utf8}")? -## ``` -## -exec_output! : - Cmd - => - Result - { stdout_utf8 : Str, stderr_utf8_lossy : Str } +import IOErr exposing [IOErr] + +Cmd :: { + args : List(Str), + clear_envs : Bool, + envs : List(Str), # TODO change this to List((Str, Str)) + program : Str, +}.{ + host_exec_exit_code! : Cmd => Try(I32, IOErr) + + host_exec_output! : Cmd => Try(OutputFromHostSuccess, Try(OutputFromHostFailure, IOErr)) + + + ## 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 = "${program} ${Str.join_with(arguments, " ")}" + 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({ problem : _, index : U64 })] }), + NonZeroExitCode({ command : Str, exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str }), + FailedToGetExitCode({ command : Str, err : IOErr }), + .. + ] + ) + exec_output! = |cmd| { + exec_try = Cmd.host_exec_output!(cmd) + + match exec_try { + Ok({ stderr_bytes, stdout_bytes }) => { + stdout_utf8 = + Str.from_utf8(stdout_bytes) + .map_err(|err| StdoutContainsInvalidUtf8({ cmd_str: to_str(cmd), err }))? + + 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 })) + } + } + } + } + } + + ## 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) }, [ - 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 }, + NonZeroExitCodeB({ exit_code : I32, stdout_bytes : List(U8), stderr_bytes : List(U8) }), + FailedToGetExitCodeB(IOErr), + .. ] -exec_output! = |@Cmd(cmd)| - exec_res = Host.command_exec_output!(cmd) - - when exec_res is - Ok({ stderr_bytes, stdout_bytes }) -> - stdout_utf8 = Str.from_utf8(stdout_bytes) ? |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_res) -> - when inside_res is - 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. -## -## > Stdin is not inherited from the parent and any attempt by the child process -## > to read from the stdin stream will result in the stream immediately closing. -## -## Use [exec_output!] instead if you want to get the output as UTF-8 strings. -## -## ``` -## cmd_output_bytes = -## Cmd.new("echo") -## |> Cmd.args(["Hi"]) -## |> Cmd.exec_output_bytes!()? -## -## Stdout.line!("${Inspect.to_str(cmd_output_bytes)}")? # {stderr_bytes: [], stdout_bytes: [72, 105, 10]} -## ``` -## -exec_output_bytes! : Cmd => Result { stderr_bytes : List U8, stdout_bytes : List U8 } [FailedToGetExitCodeB InternalIOErr.IOErr, NonZeroExitCodeB { exit_code : I32, stderr_bytes : List U8, stdout_bytes : List U8 }] -exec_output_bytes! = |@Cmd(cmd)| - exec_res = Host.command_exec_output!(cmd) - - when exec_res is - Ok({ stderr_bytes, stdout_bytes }) -> - Ok({ stdout_bytes, stderr_bytes }) - - Err(inside_res) -> - when inside_res is - Ok({ exit_code, stderr_bytes, stdout_bytes }) -> - Err(NonZeroExitCodeB({ exit_code, stdout_bytes, stderr_bytes })) - - Err(err) -> - Err(FailedToGetExitCodeB(InternalIOErr.handle_err(err))) - -## Execute command and inherit stdin, stdout and stderr from parent. Returns the exit code. -## -## 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`. -## -## ``` -## exit_code = -## Cmd.new("cat") -## |> Cmd.args(["non_existent.txt"]) -## |> Cmd.exec_exit_code!()? -## -## Stdout.line!("${Num.to_str(exit_code)}")? # "1" -## ``` -## -exec_exit_code! : Cmd => Result I32 [FailedToGetExitCode { command : Str, err : IOErr }] -exec_exit_code! = |@Cmd(cmd)| - Host.command_exec_exit_code!(cmd) - |> Result.map_err(InternalIOErr.handle_err) - |> Result.map_err(|err| FailedToGetExitCode({ command: to_str(cmd), err })) - -## Represents a command to be executed in a child process. -Cmd := InternalCmd.Command - -## Add a single environment variable to the command. -## -## ``` -## # Run "env" and add the environment variable "FOO" with value "BAR" -## Cmd.new("env") -## |> Cmd.env("FOO", "BAR") -## ``` -## -env : Cmd, Str, Str -> Cmd -env = |@Cmd(cmd), key, value| - @Cmd({ cmd & envs: List.concat(cmd.envs, [key, value]) }) - -## Add multiple environment variables to the command. -## -## ``` -## # Run "env" and add the variables "FOO" and "BAZ" -## Cmd.new("env") -## |> Cmd.envs([("FOO", "BAR"), ("BAZ", "DUCK")]) -## ``` -## -envs : Cmd, List (Str, Str) -> Cmd -envs = |@Cmd(cmd), key_values| - values = key_values |> List.join_map(|(key, value)| [key, value]) - @Cmd({ cmd & envs: List.concat(cmd.envs, values) }) - -## Clear all environment variables, and prevent inheriting from parent, only -## the environment variables provided by [env] or [envs] are available to the child. -## -## ``` -## # Represents "env" with only "FOO" environment variable set -## Cmd.new("env") -## |> Cmd.clear_envs -## |> Cmd.env("FOO", "BAR") -## ``` -## -clear_envs : Cmd -> Cmd -clear_envs = |@Cmd(cmd)| - @Cmd({ cmd & clear_envs: Bool.true }) - -## Create a new command to execute the given program in a child process. -new : Str -> Cmd -new = |program| - @Cmd( - { - program, - args: [], - envs: [], - clear_envs: Bool.false, - }, ) + exec_output_bytes! = |cmd| { + exec_try = Cmd.host_exec_output!(cmd) + + 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(err)) + } + } + } + } + + ## Execute a command and return its exit code. + ## Stdin, stdout, and stderr are inherited from the parent process. + ## + ## You should prefer using [exec!] or [exec_cmd!] instead, only use this if you want to take a specific action based on a **specific non-zero exit code**. + ## For example, `roc check` returns exit code 1 if there are errors, and exit code 2 if there are only warnings. + ## So, you could use `exec_exit_code!` to ignore warnings on `roc check`. + ## + ## ```roc + ## exit_code = Cmd.new("cat").arg("non_existent.txt").exec_exit_code!()? + ## ``` + exec_exit_code! : Cmd => Try(I32, [FailedToGetExitCode({ command : Str, err : IOErr }), ..]) + exec_exit_code! = |cmd| { + match Cmd.host_exec_exit_code!(cmd) { + Ok(num) => Ok(num) + Err(io_err) => Err(FailedToGetExitCode({ command : to_str(cmd), err: io_err })) + } + } + + ## Create a new command with the given program name. Use a function that starts with `exec_` to execute it. + ## + ## ```roc + ## cmd = Cmd.new("ls") + ## ``` + new : Str -> Cmd + new = |program| { + args: [], + clear_envs: Bool.False, + envs: [], + program, + } + + ## 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| { + ..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| { + ..cmd, + args: cmd.args.concat(new_args), + } + + ## Add a single environment variable to the command. + ## + ## + ## ```roc + ## cmd = Cmd.new("env").env("FOO", "bar") # add the environment variable "FOO" with value "bar" + ## ``` + env : Cmd, Str, Str -> Cmd + env = |cmd, key, value| { + new_envs = cmd.envs.append(key).append(value) + { args: cmd.args, clear_envs: cmd.clear_envs, envs: new_envs, program: cmd.program } + } + + ## Add multiple environment variables to the command. + ## + ## ```roc + ## cmd = Cmd.new("env").envs([("FOO", "bar"), ("BAZ", "qux")]) + ## ``` + envs : Cmd, List((Str, Str)) -> Cmd + envs = |cmd, pairs| { + new_envs = flatten_str_pairs(pairs, cmd.envs, 0) + { args: cmd.args, clear_envs: cmd.clear_envs, envs: new_envs, program: cmd.program } + } + + ## 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") + ## ``` + clear_envs : Cmd -> Cmd + clear_envs = |cmd| { + args: cmd.args, + clear_envs: Bool.True, + envs: cmd.envs, + program: cmd.program, + } +} + +# 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, +} + +flatten_str_pairs : List((Str, Str)), List(Str), U64 -> List(Str) +flatten_str_pairs = |pairs, acc, idx| { + if idx >= pairs.len() { + acc + } else { + match pairs.get(idx) { + Ok(pair) => + flatten_str_pairs(pairs, acc.append(pair.0).append(pair.1), idx + 1) + Err(_) => + acc + } + } +} + +to_str : Cmd -> Str +to_str = |cmd| { + my_trim = |trimmed_str| + if Str.is_empty(trimmed_str) { + "" + } else { + "envs: ${trimmed_str}" + } + + envs_str = + # TODO once we're using List of tuples: .map(|(key, value)| "${key}=${value}") + my_trim(Str.trim(Str.join_with(cmd.envs, " "))) + + clear_envs_str = if cmd.clear_envs { ", clear_envs: true" } else { "" } + args_str = Str.join_with(cmd.args, " ") -## Add a single argument to the command. -## ❗ Shell features like variable subsitition (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. -## -## ``` -## # Represent the command "ls -l" -## Cmd.new("ls") -## |> Cmd.arg("-l") -## ``` -## -arg : Cmd, Str -> Cmd -arg = |@Cmd(cmd), value| - @Cmd({ cmd & args: List.append(cmd.args, value) }) - -## Add multiple arguments to the command. -## ❗ Shell features like variable subsitition (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. -## -## ``` -## # Represent the command "ls -l -a" -## Cmd.new("ls") -## |> Cmd.args(["-l", "-a"]) -## ``` -## -args : Cmd, List Str -> Cmd -args = |@Cmd(cmd), values| - @Cmd({ cmd & args: List.concat(cmd.args, values) }) + "{ cmd: ${cmd.program}, args: ${args_str}${envs_str}${clear_envs_str} }" +} diff --git a/platform/Dir.roc b/platform/Dir.roc index a359fe06..ae132440 100644 --- a/platform/Dir.roc +++ b/platform/Dir.roc @@ -1,72 +1,33 @@ -module [ - IOErr, - list!, - create!, - create_all!, - delete_empty!, - delete_all!, -] +import IOErr exposing [IOErr] -import Path exposing [Path] -import InternalIOErr +Dir := [].{ + ## Creates a new, empty directory at the provided path. + ## + ## If the parent directories do not exist, they will not be created. + ## Use [Dir.create_all!] to create parent directories as needed. + create! : Str => Try({}, [DirErr(IOErr), ..]) -## Tag union of possible errors when reading and writing a file or directory. -## -## > This is the same as [`File.IOErr`](File#IOErr). -IOErr : InternalIOErr.IOErr + ## Creates a new, empty directory at the provided path, including any parent directories. + ## + ## If the directory already exists, this will succeed without error. + create_all! : Str => Try({}, [DirErr(IOErr), ..]) -## Lists the files and directories inside the directory. -## -## > [Path.list_dir!] does the same thing, except it takes a [Path] instead of a [Str]. -list! : Str => Result (List Path) [DirErr IOErr] -list! = |path| - Path.list_dir!(Path.from_str(path)) + ## Deletes an empty directory. + ## + ## Fails if the directory is not empty. Use [Dir.delete_all!] to delete + ## a directory and all its contents. + delete_empty! : Str => Try({}, [DirErr(IOErr), ..]) -## Deletes a directory if it's empty -## -## This may fail if: -## - the path doesn't exist -## - the path is not a directory -## - the directory is not empty -## - the user lacks permission to remove the directory. -## -## > [Path.delete_empty!] does the same thing, except it takes a [Path] instead of a [Str]. -delete_empty! : Str => Result {} [DirErr IOErr] -delete_empty! = |path| - Path.delete_empty!(Path.from_str(path)) + ## Deletes a directory and all of its contents recursively. + ## + ## Use with caution! + delete_all! : Str => Try({}, [DirErr(IOErr), ..]) -## Recursively deletes the directory as well as all files and directories -## inside it. -## -## This may fail if: -## - the path doesn't exist -## - the path is not a directory -## - the user lacks permission to remove the directory. -## -## > [Path.delete_all!] does the same thing, except it takes a [Path] instead of a [Str]. -delete_all! : Str => Result {} [DirErr IOErr] -delete_all! = |path| - Path.delete_all!(Path.from_str(path)) - -## Creates a directory -## -## This may fail if: -## - a parent directory does not exist -## - the user lacks permission to create a directory there -## - the path already exists. -## -## > [Path.create_dir!] does the same thing, except it takes a [Path] instead of a [Str]. -create! : Str => Result {} [DirErr IOErr] -create! = |path| - Path.create_dir!(Path.from_str(path)) - -## Creates a directory recursively adding any missing parent directories. -## -## This may fail if: -## - the user lacks permission to create a directory there -## - the path already exists -## -## > [Path.create_all!] does the same thing, except it takes a [Path] instead of a [Str]. -create_all! : Str => Result {} [DirErr IOErr] -create_all! = |path| - Path.create_all!(Path.from_str(path)) + ## Lists the contents of a directory. + ## + ## Returns the paths of all files and directories within the specified directory. + ## + ## TODO: This temporarily returns lossy Str paths. When the vendored Path + ## subset is replaced by roc-lang/path, return byte-preserving Path values. + list! : Str => Try(List(Str), [DirErr(IOErr), ..]) +} diff --git a/platform/Env.roc b/platform/Env.roc index 1253bc82..15144440 100644 --- a/platform/Env.roc +++ b/platform/Env.roc @@ -1,171 +1,32 @@ -module [ - cwd!, - dict!, - var!, - decode!, - exe_path!, - set_cwd!, - platform!, - temp_dir!, -] - -import Path exposing [Path] -import InternalPath -import EnvDecoding -import Host - -## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory) -## from the environment. File operations on relative [Path]s are relative to this directory. -cwd! : {} => Result Path [CwdUnavailable] -cwd! = |{}| - bytes = Host.cwd!({}) |> Result.with_default([]) - - if List.is_empty(bytes) then - Err(CwdUnavailable) - else - Ok(InternalPath.from_arbitrary_bytes(bytes)) - -## Sets the [current working directory](https://en.wikipedia.org/wiki/Working_directory) -## in the environment. After changing it, file operations on relative [Path]s will be relative -## to this directory. -set_cwd! : Path => Result {} [InvalidCwd] -set_cwd! = |path| - Host.set_cwd!(InternalPath.to_bytes(path)) - |> Result.map_err(|{}| InvalidCwd) - -## Gets the path to the currently-running executable. -exe_path! : {} => Result Path [ExePathUnavailable] -exe_path! = |{}| - when Host.exe_path!({}) is - Ok(bytes) -> Ok(InternalPath.from_os_bytes(bytes)) - Err({}) -> Err(ExePathUnavailable) - -## Reads the given environment variable. -## -## If the value is invalid Unicode, the invalid parts will be replaced with the -## [Unicode replacement character](https://unicode.org/glossary/#replacement_character) ('�'). -var! : Str => Result Str [VarNotFound(Str)] -var! = |name| - Host.env_var!(name) - |> Result.map_err(|{}| VarNotFound(name)) - -## Reads the given environment variable and attempts to decode it into the correct type. -## The type being decoded into will be determined by type inference. For example, -## if this ends up being used like a `Result U16 _` then the environment variable -## will be decoded as a string representation of a `U16`. Trying to decode into -## any other type will fail with a `DecodeErr`. -## -## Supported types include; -## - Strings, -## - Numbers, as long as they contain only numeric digits, up to one `.`, and an optional `-` at the front for negative numbers, and -## - Comma-separated lists (of either strings or numbers), as long as there are no spaces after the commas. -## -## For example, consider we want to decode the environment variable `NUM_THINGS`; -## -## ``` -## # Reads "NUM_THINGS" and decodes into a U16 -## get_u16_var! : Str => Result U16 [VarNotFound, DecodeErr DecodeError] [Read [Env]] -## get_u16_var! = |var| -## Env.decode!(var) -## ``` -## -## If `NUM_THINGS=123` then `get_u16_var` succeeds with the value of `123u16`. -## However if `NUM_THINGS=123456789`, then `get_u16_var` will -## fail with [DecodeErr](https://www.roc-lang.org/builtins/Decode#DecodeError) -## because `123456789` is too large to fit in a [U16](https://www.roc-lang.org/builtins/Num#U16). -## -decode! : Str => Result val [VarNotFound(Str), DecodeErr DecodeError] where val implements Decoding -decode! = |name| - when Host.env_var!(name) is - Err({}) -> Err(VarNotFound(name)) - Ok(var_str) -> - Str.to_utf8(var_str) - |> Decode.from_bytes(EnvDecoding.format({})) - |> Result.map_err(|_| DecodeErr(TooShort)) - -## Reads all the process's environment variables into a [Dict]. -## -## If any key or value contains invalid Unicode, the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) -## will be used in place of any parts of keys or values that are invalid Unicode. -dict! : {} => Dict Str Str -dict! = |{}| - Host.env_dict!({}) - |> Dict.from_list - -# ## Walks over the process's environment variables as key-value arguments to the walking function. -# ## -# ## Env.walk "Vars:\n" \state, key, value -> -# ## "- ${key}: ${value}\n" -# ## # This might produce a string such as: -# ## # -# ## # """ -# ## # Vars: -# ## # - FIRST_VAR: first value -# ## # - SECOND_VAR: second value -# ## # - THIRD_VAR: third value -# ## # -# ## # """ -# ## -# ## If any key or value contains invalid Unicode, the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) -# ## (`�`) will be used in place of any parts of keys or values that are invalid Unicode. -# walk! : state, (state, Str, Str -> state) => Result state [NonUnicodeEnv state] [Read [Env]] -# walk! = |state, walker| -# Host.env_walk! state walker -# TODO could potentially offer something like walk_non_unicode which takes (state, Result Str Str, Result Str Str) so it -# tells you when there's invalid Unicode. This is both faster than (and would give you more accurate info than) -# using regular `walk` and searching for the presence of the replacement character in the resulting -# strings. However, it's unclear whether anyone would use it. What would the use case be? Reporting -# an error that the provided command-line args weren't valid Unicode? Does that still happen these days? -# TODO need to figure out clear rules for how to convert from camelCase to SCREAMING_SNAKE_CASE. -# Note that all the env vars decoded in this way become effectively *required* vars, since if any -# of them are missing, decoding will fail. For this reason, it might make sense to use this to -# decode all the required vars only, and then decode the optional ones separately some other way. -# Alternatively, it could make sense to have some sort of tag union convention here, e.g. -# if decoding into a tag union of [Present val, Missing], then it knows what to do. -# decode_all : Result val [] [EnvDecodingFailed Str] [Env] where val implements Decoding - -ARCH : [X86, X64, ARM, AARCH64, OTHER Str] -OS : [LINUX, MACOS, WINDOWS, OTHER Str] - -## Returns the current Achitecture and Operating System. -## -## `ARCH : [X86, X64, ARM, AARCH64, OTHER Str]` -## `OS : [LINUX, MACOS, WINDOWS, OTHER Str]` -## -## Note these values are constants from when the platform is built. -## -platform! : {} => { arch : ARCH, os : OS } -platform! = |{}| - - from_rust = Host.current_arch_os!({}) - - arch = - when from_rust.arch is - "x86" -> X86 - "x86_64" -> X64 - "arm" -> ARM - "aarch64" -> AARCH64 - _ -> OTHER(from_rust.arch) - - os = - when from_rust.os is - "linux" -> LINUX - "macos" -> MACOS - "windows" -> WINDOWS - _ -> OTHER(from_rust.os) - - { arch, os } - -## This uses rust's [`std::env::temp_dir()`](https://doc.rust-lang.org/std/env/fn.temp_dir.html) -## -## !! From the Rust documentation: -## -## The temporary directory may be shared among users, or between processes with different privileges; -## thus, the creation of any files or directories in the temporary directory must use a secure method -## to create a uniquely named file. Creating a file or directory with a fixed or predictable name may -## result in “insecure temporary file” security vulnerabilities. -## -temp_dir! : {} => Path -temp_dir! = |{}| - Host.temp_dir!({}) - |> InternalPath.from_os_bytes +Env := [].{ + ## Reads the given environment variable. + ## + ## If the value is invalid Unicode, the invalid parts will be replaced with the + ## [Unicode replacement character](https://unicode.org/glossary/#replacement_character). + ## + ## 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. + ## + ## TODO: This temporarily returns a lossy Str path. When the vendored Path + ## subset is replaced by roc-lang/path, return a byte-preserving Path value. + ## + ## Returns `Err(CwdUnavailable)` if the cwd cannot be determined. + cwd! : {} => Try(Str, [CwdUnavailable, ..]) + + ## Gets the path to the currently-running executable. + ## + ## TODO: This temporarily returns a lossy Str path. When the vendored Path + ## subset is replaced by roc-lang/path, return a byte-preserving Path value. + ## + ## Returns `Err(ExePathUnavailable)` if the path cannot be determined. + exe_path! : {} => Try(Str, [ExePathUnavailable, ..]) + + ## Gets the default directory for temporary files. + ## + ## TODO: This temporarily returns a lossy Str path. When the vendored Path + ## subset is replaced by roc-lang/path, return a byte-preserving Path value. + temp_dir! : {} => Str +} diff --git a/platform/EnvDecoding.roc b/platform/EnvDecoding.roc deleted file mode 100644 index 90b88476..00000000 --- a/platform/EnvDecoding.roc +++ /dev/null @@ -1,121 +0,0 @@ -module [ - EnvFormat, - format, -] - -EnvFormat := {} implements [ - DecoderFormatting { - u8: env_u8, - u16: env_u16, - u32: env_u32, - u64: env_u64, - u128: env_u128, - i8: env_i8, - i16: env_i16, - i32: env_i32, - i64: env_i64, - i128: env_i128, - f32: env_f32, - f64: env_f64, - dec: env_dec, - bool: env_bool, - string: env_string, - list: env_list, - record: env_record, - tuple: env_tuple, - }, - ] - -format : {} -> EnvFormat -format = |{}| @EnvFormat({}) - -decode_bytes_to_num = |bytes, transformer| - when Str.from_utf8(bytes) is - Ok(s) -> - when transformer(s) is - Ok(n) -> { result: Ok(n), rest: [] } - Err(_) -> { result: Err(TooShort), rest: bytes } - - Err(_) -> { result: Err(TooShort), rest: bytes } - -env_u8 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u8)) -env_u16 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u16)) -env_u32 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u32)) -env_u64 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u64)) -env_u128 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u128)) -env_i8 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i8)) -env_i16 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i16)) -env_i32 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i32)) -env_i64 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i64)) -env_i128 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i128)) -env_f32 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_f32)) -env_f64 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_f64)) -env_dec = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_dec)) - -env_bool = Decode.custom( - |bytes, @EnvFormat({})| - when Str.from_utf8(bytes) is - Ok("true") -> { result: Ok(Bool.true), rest: [] } - Ok("false") -> { result: Ok(Bool.false), rest: [] } - _ -> { result: Err(TooShort), rest: bytes }, -) - -env_string = Decode.custom( - |bytes, @EnvFormat({})| - when Str.from_utf8(bytes) is - Ok(s) -> { result: Ok(s), rest: [] } - Err(_) -> { result: Err(TooShort), rest: bytes }, -) - -env_list = |decode_elem| - Decode.custom( - |bytes, @EnvFormat({})| - # Per our supported methods of decoding, this is either a list of strings or - # a list of numbers; in either case, the list of bytes must be Utf-8 - # decodable. So just parse it as a list of strings and pass each chunk to - # the element decoder. By construction, our element decoders expect to parse - # a whole list of bytes anyway. - decode_elems = |all_bytes, accum| - { to_parse, remainder } = - when List.split_first(all_bytes, Num.to_u8(',')) is - Ok({ before, after }) -> - { to_parse: before, remainder: Some(after) } - - Err(NotFound) -> - { to_parse: all_bytes, remainder: None } - - when Decode.decode_with(to_parse, decode_elem, @EnvFormat({})) is - { result, rest } -> - when result is - Ok(val) -> - when remainder is - Some(rest_bytes) -> decode_elems(rest_bytes, List.append(accum, val)) - None -> Done(List.append(accum, val)) - - Err(e) -> Errored(e, rest) - - when decode_elems(bytes, []) is - Errored(e, rest) -> { result: Err(e), rest } - Done(vals) -> - { result: Ok(vals), rest: [] }, - ) - -# TODO: we must currently annotate the arrows here so that the lambda sets are -# exercised, and the solver can find an ambient lambda set for the -# specialization. -env_record : _, (_, _ -> [Keep (Decoder _ _), Skip]), (_, _ -> _) -> Decoder _ _ -env_record = |_initial_state, _step_field, _finalizer| - Decode.custom( - |bytes, @EnvFormat({})| - { result: Err(TooShort), rest: bytes }, - ) - -# TODO: we must currently annotate the arrows here so that the lambda sets are -# exercised, and the solver can find an ambient lambda set for the -# specialization. -env_tuple : _, (_, _ -> [Next (Decoder _ _), TooLong]), (_ -> _) -> Decoder _ _ -env_tuple = |_initial_state, _step_elem, _finalizer| - Decode.custom( - |bytes, @EnvFormat({})| - { result: Err(TooShort), rest: bytes }, - ) diff --git a/platform/File.roc b/platform/File.roc index 0e8e1d9e..4e06619d 100644 --- a/platform/File.roc +++ b/platform/File.roc @@ -1,355 +1,42 @@ -module [ - IOErr, - Reader, - write_utf8!, - write_bytes!, - write!, - read_utf8!, - read_bytes!, - delete!, - is_dir!, - is_file!, - is_sym_link!, - exists!, - is_executable!, - is_readable!, - is_writable!, - time_accessed!, - time_modified!, - time_created!, - rename!, - type!, - open_reader!, - open_reader_with_capacity!, - read_line!, - hard_link!, - size_in_bytes!, -] +import IOErr exposing [IOErr] -import Path exposing [Path] -import InternalIOErr -import Host -import InternalPath -import Utc exposing [Utc] +File := [].{ + ## Read all bytes from a file. + read_bytes! : Str => Try(List(U8), [FileErr(IOErr), ..]) -## Tag union of possible errors when reading and writing a file or directory. -## -## **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 : InternalIOErr.IOErr + ## Write bytes to a file, replacing any existing contents. + write_bytes! : Str, List(U8) => Try({}, [FileErr(IOErr), ..]) -## Write data to a file. -## -## First encode a `val` using a given `fmt` which implements the ability [Encode.EncoderFormatting](https://www.roc-lang.org/builtins/Encode#EncoderFormatting). -## -## For example, suppose you have a `Json.utf8` which implements -## [Encode.EncoderFormatting](https://www.roc-lang.org/builtins/Encode#EncoderFormatting). -## You can use this to write [JSON](https://en.wikipedia.org/wiki/JSON) -## data to a file like this: -## -## ``` -## # Writes `{"some":"json stuff"}` to the file `output.json`: -## File.write!( -## { some: "json stuff" }, -## "output.json", -## Json.utf8, -## )? -## ``` -## -## This opens the file first and closes it after writing to it. -## If writing to the file fails, for example because of a file permissions issue, the task fails with [WriteErr]. -## -## > To write unformatted bytes to a file, you can use [File.write_bytes!] instead. -## > -## > [Path.write!] does the same thing, except it takes a [Path] instead of a [Str]. -write! : val, Str, fmt => Result {} [FileWriteErr Path IOErr] where val implements Encoding, fmt implements EncoderFormatting -write! = |val, path_str, fmt| - Path.write!(val, Path.from_str(path_str), fmt) + ## Read a file's contents as a UTF-8 string. + ## + ## If the file contains invalid UTF-8, the invalid parts will be replaced with the + ## [Unicode replacement character](https://unicode.org/glossary/#replacement_character). + read_utf8! : Str => Try(Str, [FileErr(IOErr), ..]) -## Writes bytes to a file. -## -## ``` -## # Writes the bytes 1, 2, 3 to the file `myfile.dat`. -## File.write_bytes!([1, 2, 3], "myfile.dat")? -## ``` -## -## This opens the file first and closes it after writing to it. -## -## > To format data before writing it to a file, you can use [File.write!] instead. -## > -## > [Path.write_bytes!] does the same thing, except it takes a [Path] instead of a [Str]. -write_bytes! : List U8, Str => Result {} [FileWriteErr Path IOErr] -write_bytes! = |bytes, path_str| - Path.write_bytes!(bytes, Path.from_str(path_str)) + ## Write a UTF-8 string to a file, replacing any existing contents. + write_utf8! : Str, Str => Try({}, [FileErr(IOErr), ..]) -## Writes a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). -## -## ``` -## # Writes "Hello!" encoded as UTF-8 to the file `myfile.txt`. -## File.write_utf8!("Hello!", "myfile.txt")? -## ``` -## -## This opens the file first and closes it after writing to it. -## -## > To write unformatted bytes to a file, you can use [File.write_bytes!] instead. -## > -## > [Path.write_utf8!] does the same thing, except it takes a [Path] instead of a [Str]. -write_utf8! : Str, Str => Result {} [FileWriteErr Path IOErr] -write_utf8! = |str, path_str| - Path.write_utf8!(str, Path.from_str(path_str)) + ## Delete a file. + delete! : Str => Try({}, [FileErr(IOErr), ..]) -## Deletes a file from the filesystem. -## -## Performs a [`DeleteFile`](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-deletefile) -## on Windows and [`unlink`](https://en.wikipedia.org/wiki/Unlink_(Unix)) on -## UNIX systems. On Windows, this will fail when attempting to delete a readonly -## file; the file's readonly permission must be disabled before it can be -## successfully deleted. -## -## ``` -## # Deletes the file named `myfile.dat` -## File.delete!("myfile.dat")? -## ``` -## -## > This does not securely erase the file's contents from disk; instead, the operating -## system marks the space it was occupying as safe to write over in the future. Also, the operating -## system may not immediately mark the space as free; for example, on Windows it will wait until -## the last file handle to it is closed, and on UNIX, it will not remove it until the last -## [hard link](https://en.wikipedia.org/wiki/Hard_link) to it has been deleted. -## > -## > [Path.delete!] does the same thing, except it takes a [Path] instead of a [Str]. -delete! : Str => Result {} [FileWriteErr Path IOErr] -delete! = |path_str| - Path.delete!(Path.from_str(path_str)) + ## Returns the size of a file in bytes. + size_in_bytes! : Str => Try(U64, [FileErr(IOErr), ..]) -## Reads all the bytes in a file. -## -## ``` -## # Read all the bytes in `myfile.txt`. -## bytes = File.read_bytes!("myfile.txt")? -## ``` -## -## This opens the file first and closes it after reading its contents. -## -## > To read and decode data from a file into a [Str], you can use [File.read_utf8!] instead. -## > -## > [Path.read_bytes!] does the same thing, except it takes a [Path] instead of a [Str]. -read_bytes! : Str => Result (List U8) [FileReadErr Path IOErr] -read_bytes! = |path_str| - Path.read_bytes!(Path.from_str(path_str)) + ## Checks if the file has any executable bit set. + is_executable! : Str => Try(Bool, [FileErr(IOErr), ..]) -## Reads a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text. -## -## ``` -## # Reads UTF-8 encoded text into a Str from the file "myfile.txt" -## str = File.read_utf8!("myfile.txt")? -## ``` -## -## This opens the file first and closes it after reading its contents. -## The task will fail with `FileReadUtf8Err` if the given file contains invalid UTF-8. -## -## > To read unformatted bytes from a file, you can use [File.read_bytes!] instead. -## -## > [Path.read_utf8!] does the same thing, except it takes a [Path] instead of a [Str]. -read_utf8! : Str => Result Str [FileReadErr Path IOErr, FileReadUtf8Err Path _] -read_utf8! = |path_str| - Path.read_utf8!(Path.from_str(path_str)) + ## Checks if the file has a readable owner permission bit set. + is_readable! : Str => Try(Bool, [FileErr(IOErr), ..]) + ## Checks if the file has a writable owner permission bit set. + is_writable! : Str => Try(Bool, [FileErr(IOErr), ..]) -## Creates a new [hard link](https://en.wikipedia.org/wiki/Hard_link) on the filesystem. -## -## The link path will be a link pointing to the original path. -## Note that systems often require these two paths to both be located on the same filesystem. -## -## This uses [rust's std::fs::hard_link](https://doc.rust-lang.org/std/fs/fn.hard_link.html). -## -## > [Path.hard_link!] does the same thing, except it takes a [Path] instead of a [Str]. -hard_link! : Str, Str => Result {} [LinkErr IOErr] -hard_link! = |path_str_original, path_str_link| - Path.hard_link!(Path.from_str(path_str_original), Path.from_str(path_str_link)) + ## Returns the time when the file was last accessed as nanoseconds since the Unix epoch. + time_accessed! : Str => Try(U128, [FileErr(IOErr), ..]) -## Returns True if the path exists on disk and is pointing at a directory. -## Returns False if the path exists and it is not a directory. If the path does not exist, -## this function will return `Err (PathErr PathDoesNotExist)`. -## -## This uses [rust's std::path::is_dir](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_dir). -## -## > [Path.is_dir!] does the same thing, except it takes a [Path] instead of a [Str]. -is_dir! : Str => Result Bool [PathErr IOErr] -is_dir! = |path_str| - Path.is_dir!(Path.from_str(path_str)) + ## Returns the time when the file was last modified as nanoseconds since the Unix epoch. + time_modified! : Str => Try(U128, [FileErr(IOErr), ..]) -## Returns True if the path exists on disk and is pointing at a regular file. -## Returns False if the path exists and it is not a file. If the path does not exist, -## this function will return `Err (PathErr PathDoesNotExist)`. -## -## This uses [rust's std::path::is_file](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_file). -## -## > [Path.is_file!] does the same thing, except it takes a [Path] instead of a [Str]. -is_file! : Str => Result Bool [PathErr IOErr] -is_file! = |path_str| - Path.is_file!(Path.from_str(path_str)) - -## Returns True if the path exists on disk and is pointing at a symbolic link. -## Returns False if the path exists and it is not a symbolic link. If the path does not exist, -## this function will return `Err (PathErr PathDoesNotExist)`. -## -## This uses [rust's std::path::is_symlink](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_symlink). -## -## > [Path.is_sym_link!] does the same thing, except it takes a [Path] instead of a [Str]. -is_sym_link! : Str => Result Bool [PathErr IOErr] -is_sym_link! = |path_str| - Path.is_sym_link!(Path.from_str(path_str)) - -## Returns true if the path exists on disk. -## -## This uses [rust's std::path::try_exists](https://doc.rust-lang.org/std/path/struct.Path.html#method.try_exists). -exists! : Str => Result Bool [PathErr IOErr] -exists! = |path_str| - Host.file_exists!(InternalPath.to_bytes(Path.from_str(path_str))) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Checks if the file has the execute permission for the current process. -## -## This uses rust [std::fs::Metadata](https://doc.rust-lang.org/std/fs/struct.Metadata.html). -is_executable! : Str => Result Bool [PathErr IOErr] -is_executable! = |path_str| - Host.file_is_executable!(InternalPath.to_bytes(Path.from_str(path_str))) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Checks if the file has the readable permission for the current process. -## -## This uses rust [std::fs::Metadata](https://doc.rust-lang.org/std/fs/struct.Metadata.html). -is_readable! : Str => Result Bool [PathErr IOErr] -is_readable! = |path_str| - Host.file_is_readable!(InternalPath.to_bytes(Path.from_str(path_str))) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Checks if the file has the writeable permission for the current process. -## -## This uses rust [std::fs::Metadata](https://doc.rust-lang.org/std/fs/struct.Metadata.html). -is_writable! : Str => Result Bool [PathErr IOErr] -is_writable! = |path_str| - Host.file_is_writable!(InternalPath.to_bytes(Path.from_str(path_str))) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Returns the time when the file was last accessed. -## -## This uses [rust's std::fs::Metadata::accessed](https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.accessed). -## Note that this is [not guaranteed to be correct in all cases](https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.accessed). -## -## NOTE: this function will not work on Linux if the platform was built with musl, which is the case for the normal tar.br URL release. -## See "Running Locally" in the README.md file to build without musl. -time_accessed! : Str => Result Utc [PathErr IOErr] -time_accessed! = |path_str| - Host.file_time_accessed!(InternalPath.to_bytes(Path.from_str(path_str))) - |> Result.map_ok(|time_u128| Num.to_i128(time_u128) |> Utc.from_nanos_since_epoch) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Returns the time when the file was last modified. -## -## This uses [rust's std::fs::Metadata::modified](https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.modified). -## -## NOTE: this function will not work on Linux if the platform was built with musl, which is the case for the normal tar.br URL release. -## See "Running Locally" in the README.md file to build without musl. -time_modified! : Str => Result Utc [PathErr IOErr] -time_modified! = |path_str| - Host.file_time_modified!(InternalPath.to_bytes(Path.from_str(path_str))) - |> Result.map_ok(|time_u128| Num.to_i128(time_u128) |> Utc.from_nanos_since_epoch) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Returns the time when the file was created. -## -## This uses [rust's std::fs::Metadata::created](https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.created). -## -## NOTE: this function will not work on Linux if the platform was built with musl, which is the case for the normal tar.br URL release. -## See "Running Locally" in the README.md file to build without musl. -time_created! : Str => Result Utc [PathErr IOErr] -time_created! = |path_str| - Host.file_time_created!(InternalPath.to_bytes(Path.from_str(path_str))) - |> Result.map_ok(|time_u128| Num.to_i128(time_u128) |> Utc.from_nanos_since_epoch) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Renames a file or directory. -## -## This uses [rust's std::fs::rename](https://doc.rust-lang.org/std/fs/fn.rename.html). -rename! : Str, Str => Result {} [PathErr IOErr] -rename! = |from_str, to_str| - from_bytes = InternalPath.to_bytes(Path.from_str(from_str)) - to_bytes = InternalPath.to_bytes(Path.from_str(to_str)) - Host.file_rename!(from_bytes, to_bytes) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Return the type of the path if the path exists on disk. -## This uses [rust's std::path::is_symlink](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_symlink). -## -## > [Path.type!] does the same thing, except it takes a [Path] instead of a [Str]. -type! : Str => Result [IsFile, IsDir, IsSymLink] [PathErr IOErr] -type! = |path_str| - Path.type!(Path.from_str(path_str)) - -Reader := { reader : Host.FileReader, path : Path } - -## Try to open a `File.Reader` for buffered (= part by part) reading given a path string. -## See [examples/file-read-buffered.roc](https://github.com/roc-lang/basic-cli/blob/main/examples/file-read-buffered.roc) for example usage. -## -## This uses [rust's std::io::BufReader](https://doc.rust-lang.org/std/io/struct.BufReader.html). -## -## Use [read_utf8!] if you want to get the entire file contents at once. -open_reader! : Str => Result Reader [GetFileReadErr Path IOErr] -open_reader! = |path_str| - path = Path.from_str(path_str) - - # 0 means with default capacity - Host.file_reader!(Str.to_utf8(path_str), 0) - |> Result.map_err(|err| GetFileReadErr(path, InternalIOErr.handle_err(err))) - |> Result.map_ok(|reader| @Reader({ reader, path })) - -## Try to open a `File.Reader` for buffered (= part by part) reading given a path string. -## The buffer will be created with the specified capacity. -## See [examples/file-read-buffered.roc](https://github.com/roc-lang/basic-cli/blob/main/examples/file-read-buffered.roc) for example usage. -## -## This uses [rust's std::io::BufReader](https://doc.rust-lang.org/std/io/struct.BufReader.html). -## -## Use [read_utf8!] if you want to get the entire file contents at once. -open_reader_with_capacity! : Str, U64 => Result Reader [GetFileReadErr Path IOErr] -open_reader_with_capacity! = |path_str, capacity| - path = Path.from_str(path_str) - - Host.file_reader!(Str.to_utf8(path_str), capacity) - |> Result.map_err(|err| GetFileReadErr(path, InternalIOErr.handle_err(err))) - |> Result.map_ok(|reader| @Reader({ reader, path })) - -## Try to read a line from a file given a Reader. -## The line will be provided as the list of bytes (`List U8`) until a newline (`0xA` byte). -## This list will be empty when we reached the end of the file. -## See [examples/file-read-buffered.roc](https://github.com/roc-lang/basic-cli/blob/main/examples/file-read-buffered.roc) for example usage. -## -## This uses [rust's `BufRead::read_line`](https://doc.rust-lang.org/std/io/trait.BufRead.html#method.read_line). -## -## Use [read_utf8!] if you want to get the entire file contents at once. -read_line! : Reader => Result (List U8) [FileReadErr Path IOErr] -read_line! = |@Reader({ reader, path })| - Host.file_read_line!(reader) - |> Result.map_err(|err| FileReadErr(path, InternalIOErr.handle_err(err))) - -## Returns the size of a file in bytes. -## -## This uses [rust's std::fs::Metadata::len](https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.len). -size_in_bytes! : Str => Result U64 [PathErr IOErr] -size_in_bytes! = |path_str| - Host.file_size_in_bytes!(InternalPath.to_bytes(Path.from_str(path_str))) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) + ## Returns the time when the file was created as nanoseconds since the Unix epoch. + time_created! : Str => Try(U128, [FileErr(IOErr), ..]) +} diff --git a/platform/Host.roc b/platform/Host.roc deleted file mode 100644 index 4e83fdc7..00000000 --- a/platform/Host.roc +++ /dev/null @@ -1,153 +0,0 @@ -hosted [ - FileReader, - TcpStream, - command_exec_output!, - command_exec_exit_code!, - current_arch_os!, - cwd!, - dir_create!, - dir_create_all!, - dir_delete_all!, - dir_delete_empty!, - dir_list!, - env_dict!, - env_var!, - exe_path!, - file_delete!, - file_exists!, - file_read_bytes!, - file_reader!, - file_read_line!, - file_size_in_bytes!, - file_write_bytes!, - file_write_utf8!, - file_is_executable!, - file_is_readable!, - file_is_writable!, - file_time_accessed!, - file_time_modified!, - file_time_created!, - file_rename!, - get_locale!, - get_locales!, - hard_link!, - path_type!, - posix_time!, - random_u64!, - random_u32!, - send_request!, - set_cwd!, - sleep_millis!, - sqlite_bind!, - sqlite_columns!, - sqlite_column_value!, - sqlite_prepare!, - sqlite_reset!, - sqlite_step!, - stderr_line!, - stderr_write!, - stderr_write_bytes!, - stdin_bytes!, - stdin_line!, - stdin_read_to_end!, - stdout_line!, - stdout_write!, - stdout_write_bytes!, - tcp_connect!, - tcp_read_exactly!, - tcp_read_until!, - tcp_read_up_to!, - tcp_write!, - temp_dir!, - tty_mode_canonical!, - tty_mode_raw!, -] - -import InternalHttp -import InternalCmd -import InternalPath -import InternalIOErr -import InternalSqlite -# COMMAND -command_exec_exit_code! : InternalCmd.Command => Result I32 InternalIOErr.IOErrFromHost -command_exec_output! : InternalCmd.Command => Result InternalCmd.OutputFromHostSuccess (Result InternalCmd.OutputFromHostFailure InternalIOErr.IOErrFromHost) - -# FILE -file_write_bytes! : List U8, List U8 => Result {} InternalIOErr.IOErrFromHost -file_write_utf8! : List U8, Str => Result {} InternalIOErr.IOErrFromHost -file_delete! : List U8 => Result {} InternalIOErr.IOErrFromHost -file_read_bytes! : List U8 => Result (List U8) InternalIOErr.IOErrFromHost -file_size_in_bytes! : List U8 => Result U64 InternalIOErr.IOErrFromHost -file_exists! : List U8 => Result Bool InternalIOErr.IOErrFromHost -file_is_executable! : List U8 => Result Bool InternalIOErr.IOErrFromHost -file_is_readable! : List U8 => Result Bool InternalIOErr.IOErrFromHost -file_is_writable! : List U8 => Result Bool InternalIOErr.IOErrFromHost -file_time_accessed! : List U8 => Result U128 InternalIOErr.IOErrFromHost -file_time_modified! : List U8 => Result U128 InternalIOErr.IOErrFromHost -file_time_created! : List U8 => Result U128 InternalIOErr.IOErrFromHost -file_rename! : List U8, List U8 => Result {} InternalIOErr.IOErrFromHost - -FileReader := Box {} -file_reader! : List U8, U64 => Result FileReader InternalIOErr.IOErrFromHost -file_read_line! : FileReader => Result (List U8) InternalIOErr.IOErrFromHost - -dir_list! : List U8 => Result (List (List U8)) InternalIOErr.IOErrFromHost -dir_create! : List U8 => Result {} InternalIOErr.IOErrFromHost -dir_create_all! : List U8 => Result {} InternalIOErr.IOErrFromHost -dir_delete_empty! : List U8 => Result {} InternalIOErr.IOErrFromHost -dir_delete_all! : List U8 => Result {} InternalIOErr.IOErrFromHost - -hard_link! : List U8, List U8 => Result {} InternalIOErr.IOErrFromHost -path_type! : List U8 => Result InternalPath.InternalPathType InternalIOErr.IOErrFromHost -cwd! : {} => Result (List U8) {} -temp_dir! : {} => List U8 - -# STDIO -stdout_line! : Str => Result {} InternalIOErr.IOErrFromHost -stdout_write! : Str => Result {} InternalIOErr.IOErrFromHost -stdout_write_bytes! : List U8 => Result {} InternalIOErr.IOErrFromHost -stderr_line! : Str => Result {} InternalIOErr.IOErrFromHost -stderr_write! : Str => Result {} InternalIOErr.IOErrFromHost -stderr_write_bytes! : List U8 => Result {} InternalIOErr.IOErrFromHost -stdin_line! : {} => Result Str InternalIOErr.IOErrFromHost -stdin_bytes! : {} => Result (List U8) InternalIOErr.IOErrFromHost -stdin_read_to_end! : {} => Result (List U8) InternalIOErr.IOErrFromHost - -# TCP -send_request! : InternalHttp.RequestToAndFromHost => InternalHttp.ResponseToAndFromHost - -TcpStream := Box {} -tcp_connect! : Str, U16 => Result TcpStream Str -tcp_read_up_to! : TcpStream, U64 => Result (List U8) Str -tcp_read_exactly! : TcpStream, U64 => Result (List U8) Str -tcp_read_until! : TcpStream, U8 => Result (List U8) Str -tcp_write! : TcpStream, List U8 => Result {} Str - -# SQLITE -sqlite_prepare! : Str, Str => Result (Box {}) InternalSqlite.SqliteError -sqlite_bind! : Box {}, List InternalSqlite.SqliteBindings => Result {} InternalSqlite.SqliteError -sqlite_columns! : Box {} => List Str -sqlite_column_value! : Box {}, U64 => Result InternalSqlite.SqliteValue InternalSqlite.SqliteError -sqlite_step! : Box {} => Result InternalSqlite.SqliteState InternalSqlite.SqliteError -sqlite_reset! : Box {} => Result {} InternalSqlite.SqliteError - -# OTHERS -current_arch_os! : {} => { arch : Str, os : Str } - -get_locale! : {} => Result Str {} -get_locales! : {} => List Str - -posix_time! : {} => U128 # TODO why is this a U128 but then getting converted to a I128 in Utc.roc? - -sleep_millis! : U64 => {} - -tty_mode_canonical! : {} => {} -tty_mode_raw! : {} => {} - -env_dict! : {} => List (Str, Str) -env_var! : Str => Result Str {} -exe_path! : {} => Result (List U8) {} -set_cwd! : List U8 => Result {} {} - -random_u64! : {} => Result U64 InternalIOErr.IOErrFromHost -random_u32! : {} => Result U32 InternalIOErr.IOErrFromHost diff --git a/platform/Http.roc b/platform/Http.roc index 433125f2..935a6578 100644 --- a/platform/Http.roc +++ b/platform/Http.roc @@ -1,146 +1,131 @@ -module [ - Request, - Response, - Method, - Header, - header, - default_request, - send!, - get!, - get_utf8!, -] - import InternalHttp -import Host - -## Represents an HTTP method: `[OPTIONS, GET, POST, PUT, DELETE, HEAD, TRACE, CONNECT, PATCH, EXTENSION Str]` -Method : InternalHttp.Method - -## Represents an HTTP header e.g. `Content-Type: application/json`. -## Header is a `{ name : Str, value : Str }`. -Header : InternalHttp.Header - -## Represents an HTTP request. -## Request is a record: -## ``` -## { -## method : Method, -## headers : List Header, -## uri : Str, -## body : List U8, -## timeout_ms : [TimeoutMilliseconds U64, NoTimeout], -## } -## ``` -Request : InternalHttp.Request - -## Represents an HTTP response. -## -## Response is a record with the following fields: -## ``` -## { -## status : U16, -## headers : List Header, -## body : List U8 -## } -## ``` -Response : InternalHttp.Response - -## A default [Request] value with the following values: -## ``` -## { -## method: GET -## headers: [] -## uri: "" -## body: [] -## timeout_ms: NoTimeout -## } -## ``` -## -## Example: -## ``` -## # GET "roc-lang.org" -## { Http.default_request & -## uri: "https://www.roc-lang.org", -## } -## ``` -## -default_request : Request -default_request = { - method: GET, - headers: [], - uri: "", - body: [], - timeout_ms: NoTimeout, + +Http := [].{ + ## Represents an HTTP method. + Method : InternalHttp.Method + + ## Represents an HTTP header e.g. `Content-Type: application/json`, as a + ## `{ name : Str, value : Str }` record. + Header : InternalHttp.Header + + ## Represents an HTTP request. + Request : InternalHttp.Request + + ## Represents an HTTP response. + Response : InternalHttp.Response + + # The single host effect: hand a fully-marshalled request to the host and get + # back a marshalled response. Transport failures are encoded by the host as a + # sentinel status+body pair (see `send!`), not as a separate error channel. + host_send_request! : InternalHttp.RequestToAndFromHost => InternalHttp.ResponseToAndFromHost + + ## A default [Request]: `GET` with no headers, empty uri/body, and no timeout. + ## + ## ```roc + ## { Http.default_request & uri: "https://www.roc-lang.org" } + ## ``` + default_request : Request + default_request = { + method: GET, + headers: [], + uri: "", + body: [], + timeout_ms: NoTimeout, + } + + ## Build an HTTP [Header] from a `(name, value)` tuple. + ## + ## ```roc + ## Http.header(("Content-Type", "application/json")) + ## ``` + header : (Str, Str) -> Header + header = |(name, value)| { name, value } + + ## Send an HTTP request, succeeding with a [Response] or failing with an + ## `HttpErr`. + ## + ## ```roc + ## response = Http.send!({ Http.default_request & uri: "https://www.roc-lang.org" })? + ## ``` + send! : Request => Try(Response, [HttpErr([Timeout, NetworkError, BadBody, Other(List(U8))])]) + send! = |request| { + host_request = to_host_request(request) + response = from_host_response(Http.host_send_request!(host_request)) + + # The host signals transport failures with these reserved status+body + # sentinels (produced in src/lib.rs); everything else is a real response. + other_error_prefix = Str.to_utf8("OTHER ERROR\n") + + if response.status == 408 and response.body == Str.to_utf8("Timeout") { + Err(HttpErr(Timeout)) + } else if response.status == 500 and response.body == Str.to_utf8("NetworkError") { + Err(HttpErr(NetworkError)) + } else if response.status == 500 and response.body == Str.to_utf8("BadBody") { + Err(HttpErr(BadBody)) + } else if response.status == 500 and List.starts_with(response.body, other_error_prefix) { + Err(HttpErr(Other(List.drop_first(response.body, List.len(other_error_prefix))))) + } else { + Ok(response) + } + } + + ## Perform an HTTP GET and decode the response body as a UTF-8 [Str]. + ## + ## ```roc + ## hello_str = Http.get_utf8!("http://localhost:8000")? + ## ``` + get_utf8! : Str => Try(Str, [BadBody(Str), HttpErr([Timeout, NetworkError, BadBody, Other(List(U8))])]) + get_utf8! = |uri| + match send!({ ..default_request, uri: uri }) { + Err(HttpErr(err)) => Err(HttpErr(err)) + Ok(response) => + match Str.from_utf8(response.body) { + Ok(str) => Ok(str) + Err(_) => Err(BadBody("get_utf8!: response body was not valid UTF-8")) + } + } +} + +# ---- internal conversion helpers (module-private) ------------------------------ + +# These numeric method tags must match `as_hyper_method` in src/lib.rs. +to_host_request = |request| { + method: to_host_method(request.method), + method_ext: to_host_method_ext(request.method), + headers: request.headers, + uri: request.uri, + body: request.body, + timeout_ms: to_host_timeout(request.timeout_ms), +} + +from_host_response = |response| { + status: response.status, + headers: response.headers, + body: response.body, } -## An HTTP header for configuring requests. -## -## See common headers [here](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields). -## -## Example: `header(("Content-Type", "application/json"))` -## -header : (Str, Str) -> Header -header = |(name, value)| { name, value } - -## Send an HTTP request, succeeds with a [Response] or fails with a [HttpErr _]. -## -## ``` -## # Prints out the HTML of the Roc-lang website. -## response : Response -## response = -## Http.send!({ Http.default_request & uri: "https://www.roc-lang.org" })? -## -## Stdout.line!(Str.from_utf8(response.body)?)? -## ``` -send! : Request => Result Response [HttpErr [Timeout, NetworkError, BadBody, Other (List U8)]] -send! = |request| - - host_request = InternalHttp.to_host_request(request) - - response = Host.send_request!(host_request) |> InternalHttp.from_host_response - - other_error_prefix = Str.to_utf8("OTHER ERROR\n") - - if response.status == 408 and response.body == Str.to_utf8("Request Timeout") then - Err(HttpErr(Timeout)) - else if response.status == 500 and response.body == Str.to_utf8("Network Error") then - Err(HttpErr(NetworkError)) - else if response.status == 500 and response.body == Str.to_utf8("Bad Body") then - Err(HttpErr(BadBody)) - else if response.status == 500 and List.starts_with(response.body, other_error_prefix) then - Err(HttpErr(Other(List.drop_first(response.body, List.len(other_error_prefix))))) - else - Ok(response) - -## Try to perform an HTTP get request and convert (decode) the received bytes into a Roc type. -## Very useful for working with Json. -## -## ``` -## import json.Json -## -## # On the server side we send `Encode.to_bytes({foo: "Hello Json!"}, Json.utf8)` -## { foo } = Http.get!("http://localhost:8000", Json.utf8)? -## ``` -get! : Str, fmt => Result body [HttpDecodingFailed, HttpErr _] where body implements Decoding, fmt implements DecoderFormatting -get! = |uri, fmt| - response = send!({ default_request & uri })? - - Decode.from_bytes(response.body, fmt) - |> Result.map_err(|_| HttpDecodingFailed) - -# Contributor note: Trying to use BadUtf8 { problem : Str.Utf8Problem, index : U64 } in the error here results in a "Alias `6.IdentId(11)` not registered in delayed aliases!". -## Try to perform an HTTP get request and convert the received bytes (in the body) into a UTF-8 string. -## -## ``` -## # On the server side we, send `Str.to_utf8("Hello utf8")` -## -## hello_str : Str -## hello_str = Http.get_utf8!("http://localhost:8000")? -## ``` -get_utf8! : Str => Result Str [BadBody Str, HttpErr _] -get_utf8! = |uri| - response = send!({ default_request & uri })? - - response.body - |> Str.from_utf8 - |> Result.map_err(|err| BadBody("Error in get_utf8!: failed to convert received body bytes into utf8:\n\t${Inspect.to_str(err)}")) +to_host_method = |method| + match method { + OPTIONS => 5 + GET => 3 + POST => 7 + PUT => 8 + DELETE => 1 + HEAD => 4 + TRACE => 9 + CONNECT => 0 + PATCH => 6 + EXTENSION(_) => 2 + } + +to_host_method_ext = |method| + match method { + EXTENSION(ext) => ext + _ => "" + } + +to_host_timeout = |timeout| + match timeout { + TimeoutMilliseconds(ms) => ms + NoTimeout => 0 + } diff --git a/platform/InternalIOErr.roc b/platform/IOErr.roc similarity index 54% rename from platform/InternalIOErr.roc rename to platform/IOErr.roc index 033a0da6..68e33f14 100644 --- a/platform/InternalIOErr.roc +++ b/platform/IOErr.roc @@ -1,9 +1,5 @@ -module [ - IOErr, - IOErrFromHost, - handle_err, -] - +## 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. @@ -19,40 +15,13 @@ module [ ## **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, +IOErr := [ AlreadyExists, + BrokenPipe, Interrupted, - Unsupported, + NotFound, + Other(Str), OutOfMemory, - Other Str, + PermissionDenied, + Unsupported, ] - -IOErrFromHost : { - tag : [ - EndOfFile, - NotFound, - PermissionDenied, - BrokenPipe, - AlreadyExists, - Interrupted, - Unsupported, - OutOfMemory, - Other, - ], - msg : Str, -} - -handle_err : IOErrFromHost -> IOErr -handle_err = |{ tag, msg }| - when tag is - NotFound -> NotFound - PermissionDenied -> PermissionDenied - BrokenPipe -> BrokenPipe - AlreadyExists -> AlreadyExists - Interrupted -> Interrupted - Unsupported -> Unsupported - OutOfMemory -> OutOfMemory - Other | EndOfFile -> Other(msg) diff --git a/platform/InternalArg.roc b/platform/InternalArg.roc deleted file mode 100644 index 5776fd32..00000000 --- a/platform/InternalArg.roc +++ /dev/null @@ -1,14 +0,0 @@ -module [ArgToAndFromHost, to_os_raw] - -# represented this way to simplify the glue across the host boundary -ArgToAndFromHost := { - type : [Unix, Windows], - unix : List U8, - windows : List U16, -} - -to_os_raw : ArgToAndFromHost -> [Unix (List U8), Windows (List U16)] -to_os_raw = |@ArgToAndFromHost(inner)| - when inner.type is - Unix -> Unix(inner.unix) - Windows -> Windows(inner.windows) diff --git a/platform/InternalCmd.roc b/platform/InternalCmd.roc deleted file mode 100644 index 55cc0c9c..00000000 --- a/platform/InternalCmd.roc +++ /dev/null @@ -1,41 +0,0 @@ -module [ - Command, - OutputFromHostSuccess, - OutputFromHostFailure, - to_str, -] - -Command : { - program : Str, - args : List Str, # [arg0, arg1, arg2, arg3, ...] - envs : List Str, # TODO change this to list of tuples? [key0, value0, key1, value1, key2, value2, ...] - clear_envs : Bool, -} - -# 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, -} - -to_str : Command -> Str -to_str = |cmd| - envs_str = - cmd.envs - #|> List.map(|(key, value)| "${key}=${value}") - |> Str.join_with(" ") - |> Str.trim() - |> (|trimmed_str| if Str.is_empty(trimmed_str) then "" else "envs: ${trimmed_str}") - - clear_envs_str = if cmd.clear_envs then ", clear_envs: true" else "" - - """ - { cmd: ${cmd.program}, args: ${Str.join_with(cmd.args, " ")}${envs_str}${clear_envs_str} } - """ \ No newline at end of file diff --git a/platform/InternalDateTime.roc b/platform/InternalDateTime.roc deleted file mode 100644 index 4e639927..00000000 --- a/platform/InternalDateTime.roc +++ /dev/null @@ -1,221 +0,0 @@ -module [ - DateTime, - to_iso_8601, - epoch_millis_to_datetime, -] - -DateTime : { year : I128, month : I128, day : I128, hours : I128, minutes : I128, seconds : I128 } - -to_iso_8601 : DateTime -> Str -to_iso_8601 = |{ year, month, day, hours, minutes, seconds }| - year_str = year_with_padded_zeros(year) - month_str = month_with_padded_zeros(month) - day_str = day_with_padded_zeros(day) - hour_str = hours_with_padded_zeros(hours) - minute_str = minutes_with_padded_zeros(minutes) - seconds_str = seconds_with_padded_zeros(seconds) - - "${year_str}-${month_str}-${day_str}T${hour_str}:${minute_str}:${seconds_str}Z" - -year_with_padded_zeros : I128 -> Str -year_with_padded_zeros = |year| - year_str = Num.to_str(year) - if year < 10 then - "000${year_str}" - else if year < 100 then - "00${year_str}" - else if year < 1000 then - "0${year_str}" - else - year_str - -month_with_padded_zeros : I128 -> Str -month_with_padded_zeros = |month| - month_str = Num.to_str(month) - if month < 10 then - "0${month_str}" - else - month_str - -day_with_padded_zeros : I128 -> Str -day_with_padded_zeros = month_with_padded_zeros - -hours_with_padded_zeros : I128 -> Str -hours_with_padded_zeros = month_with_padded_zeros - -minutes_with_padded_zeros : I128 -> Str -minutes_with_padded_zeros = month_with_padded_zeros - -seconds_with_padded_zeros : I128 -> Str -seconds_with_padded_zeros = month_with_padded_zeros - -is_leap_year : I128 -> Bool -is_leap_year = |year| - (year % 4 == 0) - and # divided evenly by 4 unless... - ( - (year % 100 != 0) - or # divided by 100 not a leap year - (year % 400 == 0) # expecpt when also divisible by 400 - ) - -expect is_leap_year(2000) -expect is_leap_year(2012) -expect !(is_leap_year(1900)) -expect !(is_leap_year(2015)) -expect List.map([2023, 1988, 1992, 1996], is_leap_year) == [Bool.false, Bool.true, Bool.true, Bool.true] -expect List.map([1700, 1800, 1900, 2100, 2200, 2300, 2500, 2600], is_leap_year) == [Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false] - -days_in_month : I128, I128 -> I128 -days_in_month = |year, month| - if List.contains([1, 3, 5, 7, 8, 10, 12], month) then - 31 - else if List.contains([4, 6, 9, 11], month) then - 30 - else if month == 2 then - (if is_leap_year(year) then 29 else 28) - else - 0 - -expect days_in_month(2023, 1) == 31 # January -expect days_in_month(2023, 2) == 28 # February -expect days_in_month(1996, 2) == 29 # February in a leap year -expect days_in_month(2023, 3) == 31 # March -expect days_in_month(2023, 4) == 30 # April -expect days_in_month(2023, 5) == 31 # May -expect days_in_month(2023, 6) == 30 # June -expect days_in_month(2023, 7) == 31 # July -expect days_in_month(2023, 8) == 31 # August -expect days_in_month(2023, 9) == 30 # September -expect days_in_month(2023, 10) == 31 # October -expect days_in_month(2023, 11) == 30 # November -expect days_in_month(2023, 12) == 31 # December - -epoch_millis_to_datetime : I128 -> DateTime -epoch_millis_to_datetime = |millis| - seconds = millis // 1000 - minutes = seconds // 60 - hours = minutes // 60 - day = 1 + hours // 24 - month = 1 - year = 1970 - - epoch_millis_to_datetime_help( - { - year, - month, - day, - hours: hours % 24, - minutes: minutes % 60, - seconds: seconds % 60, - }, - ) - -epoch_millis_to_datetime_help : DateTime -> DateTime -epoch_millis_to_datetime_help = |current| - count_days_in_month = days_in_month(current.year, current.month) - count_days_in_prev_month = - if current.month == 1 then - days_in_month((current.year - 1), 12) - else - days_in_month(current.year, (current.month - 1)) - - if current.day < 1 then - epoch_millis_to_datetime_help( - { current & - year: if current.month == 1 then current.year - 1 else current.year, - month: if current.month == 1 then 12 else current.month - 1, - day: current.day + count_days_in_prev_month, - }, - ) - else if current.hours < 0 then - epoch_millis_to_datetime_help( - { current & - day: current.day - 1, - hours: current.hours + 24, - }, - ) - else if current.minutes < 0 then - epoch_millis_to_datetime_help( - { current & - hours: current.hours - 1, - minutes: current.minutes + 60, - }, - ) - else if current.seconds < 0 then - epoch_millis_to_datetime_help( - { current & - minutes: current.minutes - 1, - seconds: current.seconds + 60, - }, - ) - else if current.day > count_days_in_month then - epoch_millis_to_datetime_help( - { current & - year: if current.month == 12 then current.year + 1 else current.year, - month: if current.month == 12 then 1 else current.month + 1, - day: current.day - count_days_in_month, - }, - ) - else - current - -# test 1000 ms before epoch -expect - str = -1000 |> epoch_millis_to_datetime |> to_iso_8601 - str == "1969-12-31T23:59:59Z" - -# test 1 hour, 1 minute, 1 second before epoch -expect - str = (-3600 * 1000 - 60 * 1000 - 1000) |> epoch_millis_to_datetime |> to_iso_8601 - str == "1969-12-31T22:58:59Z" - -# test 1 month before epoch -expect - str = (-1 * 31 * 24 * 60 * 60 * 1000) |> epoch_millis_to_datetime |> to_iso_8601 - str == "1969-12-01T00:00:00Z" - -# test 1 year before epoch -expect - str = (-1 * 365 * 24 * 60 * 60 * 1000) |> epoch_millis_to_datetime |> to_iso_8601 - str == "1969-01-01T00:00:00Z" - -# test 1st leap year before epoch -expect - str = (-1 * (365 + 366) * 24 * 60 * 60 * 1000) |> epoch_millis_to_datetime |> to_iso_8601 - str == "1968-01-01T00:00:00Z" - -# test last day of 1st year after epoch -expect - str = (364 * 24 * 60 * 60 * 1000) |> epoch_millis_to_datetime |> to_iso_8601 - str == "1970-12-31T00:00:00Z" - -# test last day of 1st month after epoch -expect - str = (30 * 24 * 60 * 60 * 1000) |> epoch_millis_to_datetime |> to_iso_8601 - str == "1970-01-31T00:00:00Z" - -# test 1_700_005_179_053 ms past epoch -expect - str = 1_700_005_179_053 |> epoch_millis_to_datetime |> to_iso_8601 - str == "2023-11-14T23:39:39Z" - -# test 1000 ms past epoch -expect - str = 1_000 |> epoch_millis_to_datetime |> to_iso_8601 - str == "1970-01-01T00:00:01Z" - -# test 1_000_000 ms past epoch -expect - str = 1_000_000 |> epoch_millis_to_datetime |> to_iso_8601 - str == "1970-01-01T00:16:40Z" - -# test 1_000_000_000 ms past epoch -expect - str = 1_000_000_000 |> epoch_millis_to_datetime |> to_iso_8601 - str == "1970-01-12T13:46:40Z" - -# test 1_600_005_179_000 ms past epoch -expect - str = 1_600_005_179_000 |> epoch_millis_to_datetime |> to_iso_8601 - str == "2020-09-13T13:52:59Z" diff --git a/platform/InternalHttp.roc b/platform/InternalHttp.roc index 6bf0287b..5607eee6 100644 --- a/platform/InternalHttp.roc +++ b/platform/InternalHttp.roc @@ -1,139 +1,39 @@ -# TODO we should be able to pull this out into a cross-platform package so multiple -# platforms can use it. -# -# I haven't tried that here because I just want to get the implementation working on -# both basic-cli and basic-webserver. Copy-pase is fine for now. -module [ - Request, - Response, - RequestToAndFromHost, - ResponseToAndFromHost, - Method, - Header, - to_host_request, - to_host_response, - from_host_request, - from_host_response, -] - -# FOR ROC - -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods -Method : [OPTIONS, GET, POST, PUT, DELETE, HEAD, TRACE, CONNECT, PATCH, EXTENSION Str] - -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers -Header : { name : Str, value : Str } - -Request : { - method : Method, - headers : List Header, - uri : Str, - body : List U8, - timeout_ms : [TimeoutMilliseconds U64, NoTimeout], -} - -Response : { - status : U16, - headers : List Header, - body : List U8, -} - -# FOR HOST - -RequestToAndFromHost : { - method : U64, - method_ext : Str, - headers : List Header, - uri : Str, - body : List U8, - timeout_ms : U64, -} - -ResponseToAndFromHost : { - status : U16, - headers : List Header, - body : List U8, -} - -to_host_response : Response -> ResponseToAndFromHost -to_host_response = |{ status, headers, body }| { - status, - headers, - body, -} - -to_host_request : Request -> RequestToAndFromHost -to_host_request = |{ method, headers, uri, body, timeout_ms }| { - method: to_host_method(method), - method_ext: to_host_method_ext(method), - headers, - uri, - body, - timeout_ms: to_host_timeout(timeout_ms), -} - -to_host_method : Method -> _ -to_host_method = |method| - when method is - OPTIONS -> 5 - GET -> 3 - POST -> 7 - PUT -> 8 - DELETE -> 1 - HEAD -> 4 - TRACE -> 9 - CONNECT -> 0 - PATCH -> 6 - EXTENSION(_) -> 2 - -to_host_method_ext : Method -> Str -to_host_method_ext = |method| - when method is - EXTENSION(ext) -> ext - _ -> "" - -to_host_timeout : _ -> U64 -to_host_timeout = |timeout| - when timeout is - TimeoutMilliseconds(ms) -> ms - NoTimeout -> 0 - -from_host_request : RequestToAndFromHost -> Request -from_host_request = |{ method, method_ext, headers, uri, body, timeout_ms }| { - method: from_host_method(method, method_ext), - headers, - uri, - body, - timeout_ms: from_host_timeout(timeout_ms), -} - -from_host_method : U64, Str -> Method -from_host_method = |tag, ext| - when tag is - 5 -> OPTIONS - 3 -> GET - 7 -> POST - 8 -> PUT - 1 -> DELETE - 4 -> HEAD - 9 -> TRACE - 0 -> CONNECT - 6 -> PATCH - 2 -> EXTENSION(ext) - _ -> crash("invalid tag from host") - -from_host_timeout : U64 -> [TimeoutMilliseconds U64, NoTimeout] -from_host_timeout = |timeout| - when timeout is - 0 -> NoTimeout - _ -> TimeoutMilliseconds(timeout) - -expect from_host_timeout(0) == NoTimeout -expect from_host_timeout(1) == TimeoutMilliseconds(1) - -from_host_response : ResponseToAndFromHost -> Response -from_host_response = |{ status, headers, body }| { - status, - headers, - body, +InternalHttp := [].{ + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods + Method : [OPTIONS, GET, POST, PUT, DELETE, HEAD, TRACE, CONNECT, PATCH, EXTENSION(Str)] + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers + Header : { name : Str, value : Str } + + Request : { + method : Method, + headers : List(Header), + uri : Str, + body : List(U8), + timeout_ms : [TimeoutMilliseconds(U64), NoTimeout], + } + + Response : { + status : U16, + headers : List(Header), + body : List(U8), + } + + # The host-facing shapes flatten `Method` into a numeric tag (+ extension + # string) and `timeout_ms` into a plain `U64` (0 meaning "no timeout"), so + # the generated glue stays a simple record of primitives/lists. + RequestToAndFromHost : { + method : U64, + method_ext : Str, + headers : List(Header), + uri : Str, + body : List(U8), + timeout_ms : U64, + } + + ResponseToAndFromHost : { + status : U16, + headers : List(Header), + body : List(U8), + } } diff --git a/platform/InternalPath.roc b/platform/InternalPath.roc deleted file mode 100644 index 776bd30e..00000000 --- a/platform/InternalPath.roc +++ /dev/null @@ -1,78 +0,0 @@ -module [ - UnwrappedPath, - InternalPath, - InternalPathType, - wrap, - unwrap, - to_bytes, - from_arbitrary_bytes, - from_os_bytes, -] - -InternalPath := UnwrappedPath implements [Inspect] - -UnwrappedPath : [ - # We store these separately for two reasons: - # 1. If I'm calling an OS API, passing a path I got from the OS is definitely safe. - # However, passing a Path I got from a RocStr might be unsafe; it may contain \0 - # characters, which would result in the operation happening on a totally different - # path. As such, we need to check for \0s and fail without calling the OS API if we - # find one in the path. - # 2. If I'm converting the Path to a Str, doing that conversion on a Path that was - # created from a RocStr needs no further processing. However, if it came from the OS, - # then we need to know what charset to assume it had, in order to decode it properly. - # These come from the OS (e.g. when reading a directory, calling `canonicalize`, - # or reading an environment variable - which, incidentally, are nul-terminated), - # so we know they are both nul-terminated and do not contain interior nuls. - # As such, they can be passed directly to OS APIs. - # - # Note that the nul terminator byte is right after the end of the length (into the - # unused capacity), so this can both be compared directly to other `List U8`s that - # aren't nul-terminated, while also being able to be passed directly to OS APIs. - FromOperatingSystem (List U8), - - # These come from userspace (e.g. Path.from_bytes), so they need to be checked for interior - # nuls and then nul-terminated before the host can pass them to OS APIs. - ArbitraryBytes (List U8), - - # This was created as a RocStr, so it might have interior nul bytes but it's definitely UTF-8. - # That means we can `to_str` it trivially, but have to validate before sending it to OS - # APIs that expect a nul-terminated `char*`. - # - # Note that both UNIX and Windows APIs will accept UTF-8, because on Windows the host calls - # `_setmbcp(_MB_CP_UTF8);` to set the process's Code Page to UTF-8 before doing anything else. - # See https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page#-a-vs--w-apis - # and https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmbcp?view=msvc-170 - # for more details on the UTF-8 Code Page in Windows. - FromStr Str, -] - -InternalPathType : { - is_file : Bool, - is_sym_link : Bool, - is_dir : Bool, -} - -wrap : UnwrappedPath -> InternalPath -wrap = @InternalPath - -unwrap : InternalPath -> UnwrappedPath -unwrap = |@InternalPath(raw)| raw - -## TODO do this in the host, and iterate over the Str -## bytes when possible instead of always converting to -## a heap-allocated List. -to_bytes : InternalPath -> List U8 -to_bytes = |@InternalPath(path)| - when path is - FromOperatingSystem(bytes) -> bytes - ArbitraryBytes(bytes) -> bytes - FromStr(str) -> Str.to_utf8(str) - -from_arbitrary_bytes : List U8 -> InternalPath -from_arbitrary_bytes = |bytes| - @InternalPath(ArbitraryBytes(bytes)) - -from_os_bytes : List U8 -> InternalPath -from_os_bytes = |bytes| - @InternalPath(FromOperatingSystem(bytes)) diff --git a/platform/InternalSqlite.roc b/platform/InternalSqlite.roc index 93462e70..37778f08 100644 --- a/platform/InternalSqlite.roc +++ b/platform/InternalSqlite.roc @@ -1,29 +1,26 @@ -module [ - SqliteError, - SqliteValue, - SqliteState, - SqliteBindings, -] +# Host-ABI types shared between the SQLite host functions and the Sqlite module. +# These map 1:1 to the generated Rust glue types in src/roc_platform_abi.rs. +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/Locale.roc b/platform/Locale.roc index 9c32fe72..44657f89 100644 --- a/platform/Locale.roc +++ b/platform/Locale.roc @@ -1,20 +1,13 @@ -module [ - get!, - all!, -] +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`. + ## + ## Returns `Err(NotAvailable)` if the locale cannot be determined. + get! : () => Try(Str, [NotAvailable, ..]) -import Host - -## Returns the most preferred locale for the system or application, or `NotAvailable` if the locale could not be obtained. -## -## The returned [Str] is a BCP 47 language tag, like `en-US` or `fr-CA`. -get! : {} => Result Str [NotAvailable] -get! = |{}| - Host.get_locale!({}) - |> Result.map_err(|{}| NotAvailable) - -## Returns the preferred locales for the system or application. -## -## The returned [Str] are BCP 47 language tags, like `en-US` or `fr-CA`. -all! : {} => List Str -all! = Host.get_locales! + ## Returns the preferred locales for the system or application. + ## + ## The returned [Str] are BCP 47 language tags, like `en-US` or `fr-CA`. + all! : () => List(Str) +} diff --git a/platform/Path.roc b/platform/Path.roc index 58dc7226..64608ff1 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -1,417 +1,175 @@ -module [ - Path, - IOErr, - display, - from_str, - from_bytes, - with_extension, - is_dir!, - is_file!, - is_sym_link!, - exists!, - type!, - write_utf8!, - write_bytes!, - write!, - read_utf8!, - read_bytes!, - delete!, - list_dir!, - create_dir!, - create_all!, - delete_empty!, - delete_all!, - hard_link!, - rename!, -] - -import InternalPath -import InternalIOErr -import Host - -## Represents a path to a file or directory on the filesystem. -Path : InternalPath.InternalPath - -## Tag union of possible errors when reading and writing a file or directory. -## -## > This is the same as [`File.Err`](File#Err). -IOErr : InternalIOErr.IOErr - -## Write data to a file. -## -## First encode a `val` using a given `fmt` which implements the ability [Encode.EncoderFormatting](https://www.roc-lang.org/builtins/Encode#EncoderFormatting). -## -## For example, suppose you have a `Json.utf8` which implements -## [Encode.EncoderFormatting](https://www.roc-lang.org/builtins/Encode#EncoderFormatting). -## You can use this to write [JSON](https://en.wikipedia.org/wiki/JSON) -## data to a file like this: -## -## ``` -## # Writes `{"some":"json stuff"}` to the file `output.json`: -## Path.write!( -## { some: "json stuff" }, -## Path.from_str("output.json"), -## Json.utf8, -## )? -## ``` -## -## This opens the file first and closes it after writing to it. -## If writing to the file fails, for example because of a file permissions issue, the task fails with [WriteErr]. -## -## > To write unformatted bytes to a file, you can use [Path.write_bytes!] instead. -write! : val, Path, fmt => Result {} [FileWriteErr Path IOErr] where val implements Encoding, fmt implements EncoderFormatting -write! = |val, path, fmt| - bytes = Encode.to_bytes(val, fmt) - - # TODO handle encoding errors here, once they exist - write_bytes!(bytes, path) - -## Writes bytes to a file. -## -## ``` -## # Writes the bytes 1, 2, 3 to the file `myfile.dat`. -## Path.write_bytes!([1, 2, 3], Path.from_str("myfile.dat"))? -## ``` -## -## This opens the file first and closes it after writing to it. -## -## > To format data before writing it to a file, you can use [Path.write!] instead. -write_bytes! : List U8, Path => Result {} [FileWriteErr Path IOErr] -write_bytes! = |bytes, path| - path_bytes = InternalPath.to_bytes(path) - - Host.file_write_bytes!(path_bytes, bytes) - |> Result.map_err(|err| FileWriteErr(path, InternalIOErr.handle_err(err))) - -## Writes a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). -## -## ``` -## # Writes "Hello!" encoded as UTF-8 to the file `myfile.txt`. -## Path.write_utf8!("Hello!", Path.from_str("myfile.txt"))? -## ``` -## -## This opens the file first and closes it after writing to it. -## -## > To write unformatted bytes to a file, you can use [Path.write_bytes!] instead. -write_utf8! : Str, Path => Result {} [FileWriteErr Path IOErr] -write_utf8! = |str, path| - path_bytes = InternalPath.to_bytes(path) - - Host.file_write_utf8!(path_bytes, str) - |> Result.map_err(|err| FileWriteErr(path, InternalIOErr.handle_err(err))) - -## Note that the path may not be valid depending on the filesystem where it is used. -## For example, paths containing `:` are valid on ext4 and NTFS filesystems, but not -## on FAT ones. So if you have multiple disks on the same machine, but they have -## different filesystems, then this path could be valid on one but invalid on another! -## -## It's safest to assume paths are invalid (even syntactically) until given to an operation -## which uses them to open a file. If that operation succeeds, then the path was valid -## (at the time). Otherwise, error handling can happen for that operation rather than validating -## up front for a false sense of security (given symlinks, parts of a path being renamed, etc.). -from_str : Str -> Path -from_str = |str| - FromStr(str) - |> InternalPath.wrap - -## Not all filesystems use Unicode paths. This function can be used to create a path which -## is not valid Unicode (like a [Str] is), but which is valid for a particular filesystem. -## -## Note that if the list contains any `0` bytes, sending this path to any file operations -## (e.g. `Path.read_bytes` or `WriteStream.open_path`) will fail. -from_bytes : List U8 -> Path -from_bytes = |bytes| - ArbitraryBytes(bytes) - |> InternalPath.wrap - -## Unfortunately, operating system paths do not include information about which charset -## they were originally encoded with. It's most common (but not guaranteed) that they will -## have been encoded with the same charset as the operating system's curent locale (which -## typically does not change after it is set during installation of the OS), so -## this should convert a [Path] to a valid string as long as the path was created -## with the given `Charset`. (Use `Env.charset` to get the current system charset.) -## -## For a conversion to [Str] that is lossy but does not return a [Result], see -## [display]. -## to_inner : Path -> [Str Str, Bytes (List U8)] -## Assumes a path is encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8), -## and converts it to a string using `Str.display`. -## -## This conversion is lossy because the path may contain invalid UTF-8 bytes. If that happens, -## any invalid bytes will be replaced with the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) -## instead of returning an error. As such, it's rarely a good idea to use the [Str] returned -## by this function for any purpose other than displaying it to a user. -## -## When you don't know for sure what a path's encoding is, UTF-8 is a popular guess because -## it's the default on UNIX and also is the encoding used in Roc strings. This platform also -## automatically runs applications under the [UTF-8 code page](https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page) -## on Windows. -## -## Converting paths to strings can be an unreliable operation, because operating systems -## don't record the paths' encodings. This means it's possible for the path to have been -## encoded with a different character set than UTF-8 even if UTF-8 is the system default, -## which means when [display] converts them to a string, the string may include gibberish. -## [Here is an example.](https://unix.stackexchange.com/questions/667652/can-a-file-path-be-invalid-utf-8/667863#667863) -## -## If you happen to know the `Charset` that was used to encode the path, you can use -## `to_str_using_charset` instead of [display]. -display : Path -> Str -display = |path| - when InternalPath.unwrap(path) is - FromStr(str) -> str - FromOperatingSystem(bytes) | ArbitraryBytes(bytes) -> - when Str.from_utf8(bytes) is - Ok(str) -> str - # TODO: this should use the builtin Str.display to display invalid UTF-8 chars in just the right spots, but that does not exist yet! - Err(_) -> "�" - -## Returns true if the path exists on disk and is pointing at a directory. -## Returns `Ok false` if the path exists and it is not a directory. If the path does not exist, -## this function will return `Err (PathErr PathDoesNotExist)`. -## -## This uses [rust's std::path::is_dir](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_dir). -## -## > [`File.is_dir`](File#is_dir!) does the same thing, except it takes a [Str] instead of a [Path]. -is_dir! : Path => Result Bool [PathErr IOErr] -is_dir! = |path| - res = type!(path)? - Ok((res == IsDir)) - -## Returns true if the path exists on disk and is pointing at a regular file. -## Returns `Ok false` if the path exists and it is not a file. If the path does not exist, -## this function will return `Err (PathErr PathDoesNotExist)`. -## -## This uses [rust's std::path::is_file](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_file). -## -## > [`File.is_file`](File#is_file!) does the same thing, except it takes a [Str] instead of a [Path]. -is_file! : Path => Result Bool [PathErr IOErr] -is_file! = |path| - res = type!(path)? - Ok((res == IsFile)) - -## Returns true if the path exists on disk and is pointing at a symbolic link. -## Returns `Ok false` if the path exists and it is not a symbolic link. If the path does not exist, -## this function will return `Err (PathErr PathDoesNotExist)`. -## -## This uses [rust's std::path::is_symlink](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_symlink). -## -## > [`File.is_sym_link`](File#is_sym_link!) does the same thing, except it takes a [Str] instead of a [Path]. -is_sym_link! : Path => Result Bool [PathErr IOErr] -is_sym_link! = |path| - res = type!(path)? - Ok((res == IsSymLink)) - -## Returns true if the path exists on disk. -## -## This uses [rust's std::path::try_exists](https://doc.rust-lang.org/std/path/struct.Path.html#method.try_exists). -## -## > [`File.exists!`](File#exists!) does the same thing, except it takes a [Str] instead of a [Path]. -exists! : Path => Result Bool [PathErr IOErr] -exists! = |path| - Host.file_exists!(InternalPath.to_bytes(path)) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - -## Return the type of the path if the path exists on disk. -## -## > [`File.type`](File#type!) does the same thing, except it takes a [Str] instead of a [Path]. -type! : Path => Result [IsFile, IsDir, IsSymLink] [PathErr IOErr] -type! = |path| - Host.path_type!(InternalPath.to_bytes(path)) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) - |> Result.map_ok( - |path_type| - if path_type.is_sym_link then - IsSymLink - else if path_type.is_dir then - IsDir - else - IsFile, - ) - -## If the last component of this path has no `.`, appends `.` followed by the given string. -## Otherwise, replaces everything after the last `.` with the given string. -## -## ``` -## # Each of these gives "foo/bar/baz.txt" -## Path.from_str("foo/bar/baz") |> Path.with_extension("txt") -## Path.from_str("foo/bar/baz.") |> Path.with_extension("txt") -## Path.from_str("foo/bar/baz.xz") |> Path.with_extension("txt") -## ``` -with_extension : Path, Str -> Path -with_extension = |path, extension| - when InternalPath.unwrap(path) is - FromOperatingSystem(bytes) | ArbitraryBytes(bytes) -> - before_dot = - when List.split_last(bytes, Num.to_u8('.')) is - Ok({ before }) -> before - Err(NotFound) -> bytes - - before_dot - |> List.reserve((Str.count_utf8_bytes(extension) |> Num.int_cast |> Num.add_saturated(1))) - |> List.append(Num.to_u8('.')) - |> List.concat(Str.to_utf8(extension)) - |> ArbitraryBytes - |> InternalPath.wrap - - FromStr(str) -> - before_dot = - when Str.split_last(str, ".") is - Ok({ before }) -> before - Err(NotFound) -> str - - before_dot - |> Str.reserve((Str.count_utf8_bytes(extension) |> Num.add_saturated(1))) - |> Str.concat(".") - |> Str.concat(extension) - |> FromStr - |> InternalPath.wrap - -## Deletes a file from the filesystem. -## -## Performs a [`DeleteFile`](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-deletefile) -## on Windows and [`unlink`](https://en.wikipedia.org/wiki/Unlink_(Unix)) on -## UNIX systems. On Windows, this will fail when attempting to delete a readonly -## file; the file's readonly permission must be disabled before it can be -## successfully deleted. -## -## ``` -## # Deletes the file named `myfile.dat` -## Path.delete!(Path.from_str("myfile.dat"))? -## ``` -## -## > This does not securely erase the file's contents from disk; instead, the operating -## system marks the space it was occupying as safe to write over in the future. Also, the operating -## system may not immediately mark the space as free; for example, on Windows it will wait until -## the last file handle to it is closed, and on UNIX, it will not remove it until the last -## [hard link](https://en.wikipedia.org/wiki/Hard_link) to it has been deleted. -## -## > [`File.delete!`](File#delete!) does the same thing, except it takes a [Str] instead of a [Path]. -delete! : Path => Result {} [FileWriteErr Path IOErr] -delete! = |path| - Host.file_delete!(InternalPath.to_bytes(path)) - |> Result.map_err(|err| FileWriteErr(path, InternalIOErr.handle_err(err))) - -## Reads a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text. -## -## ``` -## # Reads UTF-8 encoded text into a Str from the file "myfile.txt" -## contents_str = Path.read_utf8!(Path.from_str("myfile.txt"))? -## ``` -## -## This opens the file first and closes it after reading its contents. -## The task will fail with `FileReadUtf8Err` if the given file contains invalid UTF-8. -## -## > To read unformatted bytes from a file, you can use [Path.read_bytes!] instead. -## > -## > [`File.read_utf8!`](File#read_utf8!) does the same thing, except it takes a [Str] instead of a [Path]. -read_utf8! : Path => Result Str [FileReadErr Path IOErr, FileReadUtf8Err Path _] -read_utf8! = |path| - bytes = - Host.file_read_bytes!(InternalPath.to_bytes(path)) - |> Result.map_err(|read_err| FileReadErr(path, InternalIOErr.handle_err(read_err)))? - - Str.from_utf8(bytes) - |> Result.map_err(|err| FileReadUtf8Err(path, err)) - -## Reads all the bytes in a file. -## -## ``` -## # Read all the bytes in `myfile.txt`. -## contents_bytes = Path.read_bytes!(Path.from_str("myfile.txt"))? -## ``` -## -## This opens the file first and closes it after reading its contents. -## -## > To read and decode data from a file into a [Str], you can use [Path.read_utf8!] instead. -## > -## > [`File.read_bytes`](File#read_bytes!) does the same thing, except it takes a [Str] instead of a [Path]. -read_bytes! : Path => Result (List U8) [FileReadErr Path IOErr] -read_bytes! = |path| - Host.file_read_bytes!(InternalPath.to_bytes(path)) - |> Result.map_err(|err| FileReadErr(path, InternalIOErr.handle_err(err))) - -## Lists the files and directories inside the directory. -## -## > [`Dir.list`](Dir#list!) does the same thing, except it takes a [Str] instead of a [Path]. -list_dir! : Path => Result (List Path) [DirErr IOErr] -list_dir! = |path| - when Host.dir_list!(InternalPath.to_bytes(path)) is - Ok(entries) -> Ok(List.map(entries, InternalPath.from_os_bytes)) - Err(err) -> Err(DirErr(InternalIOErr.handle_err(err))) - -## Deletes a directory if it's empty -## -## This may fail if: -## - the path doesn't exist -## - the path is not a directory -## - the directory is not empty -## - the user lacks permission to remove the directory. -## -## > [`Dir.delete_empty`](Dir#delete_empty!) does the same thing, except it takes a [Str] instead of a [Path]. -delete_empty! : Path => Result {} [DirErr IOErr] -delete_empty! = |path| - Host.dir_delete_empty!(InternalPath.to_bytes(path)) - |> Result.map_err(|err| DirErr(InternalIOErr.handle_err(err))) - -## Recursively deletes a directory as well as all files and directories -## inside it. -## -## This may fail if: -## - the path doesn't exist -## - the path is not a directory -## - the user lacks permission to remove the directory. -## -## > [`Dir.delete_all`](Dir#delete_all!) does the same thing, except it takes a [Str] instead of a [Path]. -delete_all! : Path => Result {} [DirErr IOErr] -delete_all! = |path| - Host.dir_delete_all!(InternalPath.to_bytes(path)) - |> Result.map_err(|err| DirErr(InternalIOErr.handle_err(err))) - -## Creates a directory -## -## This may fail if: -## - a parent directory does not exist -## - the user lacks permission to create a directory there -## - the path already exists. -## -## > [`Dir.create`](Dir#create!) does the same thing, except it takes a [Str] instead of a [Path]. -create_dir! : Path => Result {} [DirErr IOErr] -create_dir! = |path| - Host.dir_create!(InternalPath.to_bytes(path)) - |> Result.map_err(|err| DirErr(InternalIOErr.handle_err(err))) - -## Creates a directory recursively adding any missing parent directories. -## -## This may fail if: -## - the user lacks permission to create a directory there -## - the path already exists -## -## > [`Dir.create_all`](Dir#create_all!) does the same thing, except it takes a [Str] instead of a [Path]. -create_all! : Path => Result {} [DirErr IOErr] -create_all! = |path| - Host.dir_create_all!(InternalPath.to_bytes(path)) - |> Result.map_err(|err| DirErr(InternalIOErr.handle_err(err))) - -## Creates a new [hard link](https://en.wikipedia.org/wiki/Hard_link) on the filesystem. -## -## The link path will be a link pointing to the original path. -## Note that systems often require these two paths to both be located on the same filesystem. -## -## This uses [rust's std::fs::hard_link](https://doc.rust-lang.org/std/fs/fn.hard_link.html). -## -## > [File.hard_link!] does the same thing, except it takes a [Str] instead of a [Path]. -hard_link! : Path, Path => Result {} [LinkErr IOErr] -hard_link! = |path_original, path_link| - Host.hard_link!(InternalPath.to_bytes(path_original), InternalPath.to_bytes(path_link)) - |> Result.map_err(InternalIOErr.handle_err) - |> Result.map_err(LinkErr) - -## Renames a file or directory. -## -## This uses [rust's std::fs::rename](https://doc.rust-lang.org/std/fs/fn.rename.html). -rename! : Path, Path => Result {} [PathErr IOErr] -rename! = |from, to| - from_path_bytes = InternalPath.to_bytes(from) - to_path_bytes = InternalPath.to_bytes(to) - Host.file_rename!(from_path_bytes, to_path_bytes) - |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) +import IOErr exposing [IOErr] + +# TODO: This is a temporary vendored subset of roc-lang/path until packages can +# be used here. The long-term API should preserve OS paths as raw Unix bytes or +# Windows U16s end-to-end; some current Env and Dir helpers still expose lossy +# Str paths during the migration. + +PathType : { + is_file : Bool, + is_sym_link : Bool, + is_dir : Bool, +} + +Path :: [ + # We have these different internal representations for two reasons: + # 1. If I'm calling an OS API, passing a path I got from the OS is definitely safe. + # However, passing a Path I got from a RocStr might be unsafe; it may contain \0 + # characters, which would result in the operation happening on a totally different + # path. As such, we need to check for \0s and fail without calling the OS API if we + # find one in the path. + # 2. If I'm converting the Path to a Str, doing that conversion on a Path that was + # created from a RocStr needs no further processing. However, if it came from the OS, + # then we need to know what charset to assume it had, in order to decode it properly. + # These come from the OS (e.g. when reading a directory, calling `canonicalize`, + # or reading an environment variable - which, incidentally, are nul-terminated), + # so we know they are both nul-terminated and do not contain interior nuls. + # As such, they can be passed directly to OS APIs. + # + # Note that the nul terminator byte is right after the end of the length (into the + # unused capacity), so this can both be compared directly to other `List U8`s that + # aren't nul-terminated, while also being able to be passed directly to OS APIs. + FromOperatingSystem(List(U8)), + + # These come from userspace (e.g. Path.from_bytes), so they need to be checked for interior + # nuls and then nul-terminated before the host can pass them to OS APIs. + ArbitraryBytes(List(U8)), + + # This was created as a RocStr, so it might have interior nul bytes but it's definitely UTF-8. + # That means we can `to_str` it trivially, but have to validate before sending it to OS + # APIs that expect a nul-terminated `char*`. + # + # Note that both UNIX and Windows APIs will accept UTF-8, because on Windows the host calls + # `_setmbcp(_MB_CP_UTF8);` to set the process's Code Page to UTF-8 before doing anything else. + # See https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page#-a-vs--w-apis + # and https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmbcp?view=msvc-170 + # for more details on the UTF-8 Code Page in Windows. + FromStr(Str), +].{ + host_path_type! : List(U8) => Try(PathType, IOErr) + + ## 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 + ## destination file. In case of broken symbolic links this will return `Bool.False`. + is_file! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_file! = |path| + match type!(path) { + Ok(IsFile) => Ok(Bool.True) + Ok(_) => Ok(Bool.False) + Err(err) => Err(err) + } + + ## Returns `Bool.True` if the path exists on disk and is pointing at a directory. + ## + ## This function will traverse symbolic links to query information about the + ## destination file. In case of broken symbolic links this will return `Bool.False`. + is_dir! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_dir! = |path| + match type!(path) { + Ok(IsDir) => Ok(Bool.True) + Ok(_) => Ok(Bool.False) + Err(err) => Err(err) + } + + ## Returns `Bool.True` if the path exists on disk and is pointing at a symbolic link. + ## + ## This function will not traverse symbolic links - it checks whether the path + ## itself is a symlink. + is_sym_link! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_sym_link! = |path| + match type!(path) { + Ok(IsSymLink) => Ok(Bool.True) + Ok(_) => Ok(Bool.False) + Err(err) => Err(err) + } + + ## Unfortunately, operating system paths do not include information about which charset + ## they were originally encoded with. It's most common (but not guaranteed) that they will + ## have been encoded with the same charset as the operating system's curent locale (which + ## typically does not change after it is set during installation of the OS), so + ## this should convert a [Path] to a valid string as long as the path was created + ## with the given `Charset`. (Use `Env.charset` to get the current system charset.) + ## + ## For a conversion to [Str] that is lossy but does not return a [Try], see + ## [display]. + ## to_inner : Path -> [Str Str, Bytes (List U8)] + + ## Assumes a path is encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8), + ## and converts it to a string using `Str.from_utf8_lossy`. + ## + ## This conversion is lossy because the path may contain invalid UTF-8 bytes. If that happens, + ## any invalid bytes will be replaced with the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) + ## instead of returning an error. As such, it's rarely a good idea to use the [Str] returned + ## by this function for any purpose other than displaying it to a user. + ## + ## When you don't know for sure what a path's encoding is, UTF-8 is a popular guess because + ## it's the default on UNIX and also is the encoding used in Roc strings. This platform also + ## automatically runs applications under the [UTF-8 code page](https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page) + ## on Windows. + ## + ## Converting paths to strings can be an unreliable operation, because operating systems + ## don't record the paths' encodings. This means it's possible for the path to have been + ## encoded with a different character set than UTF-8 even if UTF-8 is the system default, + ## which means when [display] converts them to a string, the string may include gibberish. + ## [Here is an example.](https://unix.stackexchange.com/questions/667652/can-a-file-path-be-invalid-utf-8/667863#667863) + ## + ## If you happen to know the `Charset` that was used to encode the path, you can use + ## `to_str_using_charset` (TODO) instead of [display]. + display : Path -> Str + display = |path| + match path { + FromStr(str) => str + + FromOperatingSystem(bytes) | ArbitraryBytes(bytes) => + + match Str.from_utf8(bytes) { + Ok(str) => str + Err(_) => Str.from_utf8_lossy(bytes) + } + } + + ## Note that the path may not be valid depending on the filesystem where it is used. + ## For example, paths containing `:` are valid on ext4 and NTFS filesystems, but not + ## on FAT ones. So if you have multiple disks on the same machine, but they have + ## different filesystems, then this path could be valid on one but invalid on another! + ## + ## It's safest to assume paths are invalid (even syntactically) until given to an operation + ## which uses them to open a file. If that operation succeeds, then the path was valid + ## (at the time). Otherwise, error handling can happen for that operation rather than validating + ## up front for a false sense of security (given symlinks, parts of a path being renamed, etc.). + from_str : Str -> Path + from_str = |str| + FromStr(str) + + # TODO add charset and to_str_using_charset function, see display comment + + ## Return the type of the path if the path exists on disk. + ## + ## > [`File.type`](File#type!) does the same thing, except it takes a [Str] instead of a [Path]. + type! : Path => Try([IsFile, IsDir, IsSymLink], [PathErr(IOErr), ..]) + type! = |path| { + Path.host_path_type!(to_bytes(path)) + .map_err(|err| PathErr(err)) + .map_ok(|path_type|{ + if path_type.is_sym_link { + IsSymLink + } else if path_type.is_dir { + IsDir + } else { + IsFile + } + }) + } +} + +## TODO do this in the host, and iterate over the Str +## bytes when possible instead of always converting to +## a heap-allocated List. +to_bytes : Path -> List(U8) +to_bytes = |path| + match path { + FromOperatingSystem(bytes) => bytes + ArbitraryBytes(bytes) => bytes + FromStr(str) => Str.to_utf8(str) + } diff --git a/platform/Random.roc b/platform/Random.roc index ac11df2b..4f1d68c2 100644 --- a/platform/Random.roc +++ b/platform/Random.roc @@ -1,52 +1,9 @@ -module [ - IOErr, - random_seed_u64!, - random_seed_u32!, -] +import IOErr exposing [IOErr] -import InternalIOErr -import Host +Random := [].{ + ## Generate a random 64-bit unsigned integer seed. + seed_u64! : {} => Try(U64, [RandomErr(IOErr), ..]) - -## Tag union of possible errors when getting a random seed. -## -## > This is the same as [`File.IOErr`](File#IOErr). -IOErr : InternalIOErr.IOErr - -## Generate a random `U64` seed using the system's source of randomness. -## A "seed" is a starting value used to deterministically generate a random sequence. -## -## > !! This function is NOT cryptographically secure. -## -## This uses the [`u64()`](https://docs.rs/getrandom/latest/getrandom/fn.u64.html) function -## of the [getrandom crate](https://crates.io/crates/getrandom) to produce -## a single random 64-bit integer. -## -## For hobby purposes, you can just call this function repreatedly to get random numbers. -## In general, we recommend using this seed in combination with a library like -## [roc-random](https://github.com/lukewilliamboswell/roc-random) to generate additional -## random numbers quickly. -## -random_seed_u64! : {} => Result U64 [RandomErr IOErr] -random_seed_u64! = |{}| - Host.random_u64!({}) - |> Result.map_err(|err| RandomErr(InternalIOErr.handle_err(err))) - -## Generate a random `U32` seed using the system's source of randomness. -## A "seed" is a starting value used to deterministically generate a random sequence. -## -## > !! This function is NOT cryptographically secure. -## -## This uses the [`u32()`](https://docs.rs/getrandom/0.3.3/getrandom/fn.u32.html) function -## of the [getrandom crate](https://crates.io/crates/getrandom) to produce -## a single random 32-bit integer. -## -## For hobby purposes, you can just call this function repreatedly to get random numbers. -## In general, we recommend using this seed in combination with a library like -## [roc-random](https://github.com/lukewilliamboswell/roc-random) to generate additional -## random numbers quickly. -## -random_seed_u32! : {} => Result U32 [RandomErr IOErr] -random_seed_u32! = |{}| - Host.random_u32!({}) - |> Result.map_err(|err| RandomErr(InternalIOErr.handle_err(err))) \ No newline at end of file + ## Generate a random 32-bit unsigned integer seed. + seed_u32! : {} => Try(U32, [RandomErr(IOErr), ..]) +} diff --git a/platform/Sleep.roc b/platform/Sleep.roc index 68f97966..68f81031 100644 --- a/platform/Sleep.roc +++ b/platform/Sleep.roc @@ -1,12 +1,4 @@ -module [ - millis!, -] - -import Host - -## Sleep for at least the given number of milliseconds. -## This uses [rust's std::thread::sleep](https://doc.rust-lang.org/std/thread/fn.sleep.html). -## -millis! : U64 => {} -millis! = |milliseconds| - Host.sleep_millis!(milliseconds) +Sleep := [].{ + ## Sleep for the specified number of milliseconds. + millis! : U64 => {} +} diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index 3f47a612..6bafc8da 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -1,802 +1,481 @@ -module [ - Value, - ErrCode, - Binding, - Stmt, - SqlDecodeErr, - query!, - query_many!, - execute!, - prepare!, - query_prepared!, - query_many_prepared!, - execute_prepared!, - errcode_to_str, - decode_record, - map_value, - map_value_result, - tagged_value, - str, - bytes, - i64, - i32, - i16, - i8, - u64, - u32, - u16, - u8, - f64, - f32, - Nullable, - nullable_str, - nullable_bytes, - nullable_i64, - nullable_i32, - nullable_i16, - nullable_i8, - nullable_u64, - nullable_u32, - nullable_u16, - nullable_u8, - nullable_f64, - nullable_f32, -] - -import Host import InternalSqlite -## Represents a value that can be stored in a Sqlite database. -## -## ``` -## [ -## Null, -## Real F64, -## Integer I64, -## String Str, -## Bytes (List U8), -## ] -## ``` -Value : InternalSqlite.SqliteValue - -## Represents various error codes that can be returned by Sqlite. -## ``` -## [ -## Error, # SQL error or missing database -## Internal, # Internal logic error in Sqlite -## Perm, # Access permission denied -## Abort, # Callback routine requested an abort -## Busy, # The database file is locked -## Locked, # A table in the database is locked -## NoMem, # A malloc() failed -## ReadOnly, # Attempt to write a readonly database -## Interrupt, # Operation terminated by sqlite3_interrupt( -## IOErr, # Some kind of disk I/O error occurred -## Corrupt, # The database disk image is malformed -## NotFound, # Unknown opcode in sqlite3_file_control() -## Full, # Insertion failed because database is full -## CanNotOpen, # Unable to open the database file -## Protocol, # Database lock protocol error -## Empty, # Database is empty -## Schema, # The database schema changed -## TooBig, # String or BLOB exceeds size limit -## Constraint, # Abort due to constraint violation -## Mismatch, # Data type mismatch -## Misuse, # Library used incorrectly -## NoLfs, # Uses OS features not supported on host -## AuthDenied, # Authorization denied -## Format, # Auxiliary database format error -## OutOfRange, # 2nd parameter to sqlite3_bind out of range -## NotADatabase, # File opened that is not a database file -## Notice, # Notifications from sqlite3_log() -## Warning, # Warnings from sqlite3_log() -## Row, # sqlite3_step() has another row ready -## Done, # sqlite3_step() has finished executing -## Unknown I64, # error code not known -## ] -## ``` -ErrCode : [ - Error, # SQL error or missing database - Internal, # Internal logic error in Sqlite - Perm, # Access permission denied - Abort, # Callback routine requested an abort - Busy, # The database file is locked - Locked, # A table in the database is locked - NoMem, # A malloc() failed - ReadOnly, # Attempt to write a readonly database - Interrupt, # Operation terminated by sqlite3_interrupt( - IOErr, # Some kind of disk I/O error occurred - Corrupt, # The database disk image is malformed - NotFound, # Unknown opcode in sqlite3_file_control() - Full, # Insertion failed because database is full - CanNotOpen, # Unable to open the database file - Protocol, # Database lock protocol error - Empty, # Database is empty - Schema, # The database schema changed - TooBig, # String or BLOB exceeds size limit - Constraint, # Abort due to constraint violation - Mismatch, # Data type mismatch - Misuse, # Library used incorrectly - NoLFS, # Uses OS features not supported on host - AuthDenied, # Authorization denied - Format, # Auxiliary database format error - OutOfRange, # 2nd parameter to sqlite3_bind out of range - NotADatabase, # File opened that is not a database file - Notice, # Notifications from sqlite3_log() - Warning, # Warnings from sqlite3_log() - Row, # sqlite3_step() has another row ready - Done, # sqlite3_step() has finished executing - Unknown I64, # error code not known -] - -## Bind a name and a value to pass to the Sqlite database. -## ``` -## { -## name : Str, -## value : SqliteValue, -## } -## ``` -Binding : InternalSqlite.SqliteBindings - -## Represents a prepared statement that can be executed many times. -Stmt := Box {} - -## Prepare a [Stmt] for execution at a later time. -## -## This is useful when you have a query that will be called many times, as it is more efficient than -## preparing the query each time it is called. This is usually done in `init!` with the prepared `Stmt` stored in the model. -## -## ``` -## prepared_query = Sqlite.prepare!({ -## path : "path/to/database.db", -## query : "SELECT * FROM todos;", -## })? -## -## Sqlite.query_many_prepared!({ -## stmt: prepared_query, -## bindings: [], -## rows: { Sqlite.decode_record <- -## id: Sqlite.i64("id"), -## task: Sqlite.str("task"), -## }, -## })? -## ``` -prepare! : - { - path : Str, - query : Str, +# Porting notes for the new (zig) compiler: the decoder combinator API is written +# with fully-literal nested lambdas (`|name| |cols| |stmt| ...`) and relies on +# structural type inference. This is deliberate — the current compiler (a) treats +# an associated member whose body returns a function via a non-lambda expression as +# a hosted declaration, and (b) does not unify an associated `:` type alias (e.g. +# `Value`) with the structural tag union it aliases when used as a function +# parameter, nor support open tag-union extension (`[Tag]ext`) in annotations. So +# the decoders are left unannotated and all decoder errors live in one closed set +# of tags (see DecodeErr below for the documented shape). +Sqlite := [].{ + ## Represents a prepared statement that can be executed many times. + Stmt :: Box(U64) + + # ---- Host functions (the FFI boundary) ------------------------------------- + + host_prepare! : Str, Str => Try(Stmt, InternalSqlite.SqliteError) + + host_bind! : Stmt, List(InternalSqlite.SqliteBindings) => Try({}, InternalSqlite.SqliteError) + + host_columns! : Stmt => List(Str) + + host_column_value! : Stmt, U64 => Try(InternalSqlite.SqliteValue, InternalSqlite.SqliteError) + + # Returns Bool.True for SQLITE_ROW, Bool.False for SQLITE_DONE (the glue + # generator mishandles a bare `[Row, Done]` enum at the host boundary). + host_step! : Stmt => Try(Bool, InternalSqlite.SqliteError) + + host_reset! : Stmt => Try({}, InternalSqlite.SqliteError) + + ## Represents various error codes that can be returned by Sqlite. + ErrCode : [ + Error, + Internal, + Perm, + Abort, + Busy, + Locked, + NoMem, + ReadOnly, + Interrupt, + IOErr, + Corrupt, + NotFound, + Full, + CanNotOpen, + Protocol, + Empty, + Schema, + TooBig, + Constraint, + Mismatch, + Misuse, + NoLFS, + AuthDenied, + Format, + OutOfRange, + NotADatabase, + Notice, + Warning, + Row, + Done, + Unknown(I64), + ] + + ## Documented shape of the errors a decoder can produce (the decoders below are + ## left unannotated and infer a subset of these structurally): + ## ``` + ## [ + ## NoSuchField(Str), + ## SqliteErr(ErrCode, Str), + ## UnexpectedType([Integer, Real, String, Bytes, Null]), + ## FailedToDecodeInteger, + ## FailedToDecodeReal, + ## IntOutOfBounds, + ## NoRowsReturned, + ## TooManyRowsReturned, + ## ] + ## ``` + DecodeErr : [NoSuchField(Str), SqliteErr(ErrCode, Str)] + + # ---- Statement lifecycle --------------------------------------------------- + + ## Prepare a [Stmt] for execution at a later time. + prepare! = |{ path, query: q }| + Sqlite.host_prepare!(path, q) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + + ## Bind named parameters to a prepared statement. + bind! = |stmt, bindings| + Sqlite.host_bind!(stmt, bindings) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + + ## Return the column names for a prepared statement. + columns! = |stmt| + Sqlite.host_columns!(stmt) + + ## Read the value of a column (by index) from the current row. + column_value! = |stmt, i| + Sqlite.host_column_value!(stmt, i) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + + ## Advance a prepared statement. Returns `Row` if a row is available, `Done` otherwise. + step! = |stmt| + match Sqlite.host_step!(stmt) { + Ok(has_row) => if has_row { Ok(Row) } else { Ok(Done) } + Err({ code, message }) => Err(SqliteErr(code_from_i64(code), message)) + } + + ## Reset a prepared statement back to its initial state, ready to be re-executed. + reset! = |stmt| + Sqlite.host_reset!(stmt) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + + ## Execute a SQL statement that **doesn't return any rows** (INSERT/UPDATE/DELETE). + execute! = |{ path, query: q, bindings }| { + stmt = prepare!({ path, query: q })? + execute_prepared!({ stmt, bindings }) } - => Result Stmt [SqliteErr ErrCode Str] -prepare! = |{ path, query: q }| - Host.sqlite_prepare!(path, q) - |> Result.map_ok(@Stmt) - |> Result.map_err(internal_to_external_error) - -# internal use only -bind! : Stmt, List Binding => Result {} [SqliteErr ErrCode Str] -bind! = |@Stmt(stmt), bindings| - Host.sqlite_bind!(stmt, bindings) - |> Result.map_err(internal_to_external_error) - -# internal use only -columns! : Stmt => List Str -columns! = |@Stmt(stmt)| - Host.sqlite_columns!(stmt) - -# internal use only -column_value! : Stmt, U64 => Result Value [SqliteErr ErrCode Str] -column_value! = |@Stmt(stmt), i| - Host.sqlite_column_value!(stmt, i) - |> Result.map_err(internal_to_external_error) - -# internal use only -step! : Stmt => Result [Row, Done] [SqliteErr ErrCode Str] -step! = |@Stmt(stmt)| - Host.sqlite_step!(stmt) - |> Result.map_err(internal_to_external_error) - -# internal use only -## Resets a prepared statement back to its initial state, ready to be re-executed. -reset! : Stmt => Result {} [SqliteErr ErrCode Str] -reset! = |@Stmt(stmt)| - Host.sqlite_reset!(stmt) - |> Result.map_err(internal_to_external_error) - -## Execute a SQL statement that **doesn't return any rows** (like INSERT, UPDATE, DELETE). -## Use a function starting with `query_` if you expect rows to be returned. -## -## Use execute_prepared! if you expect to run the same query multiple times. -## -## Example: -## ``` -## Sqlite.execute!({ -## path: "path/to/database.db", -## query: "INSERT INTO users (first, last) VALUES (:first, :last);", -## bindings: [ -## { name: ":first", value: String("John") }, -## { name: ":last", value: String("Smith") }, -## ], -## })? -## ``` -execute! : - { - path : Str, - query : Str, - bindings : List Binding, + + ## Execute a prepared SQL statement that **doesn't return any rows**. + execute_prepared! = |{ stmt, bindings }| { + bind!(stmt, bindings)? + res = step!(stmt) + reset!(stmt)? + match res { + Ok(Done) => Ok({}) + Ok(Row) => Err(RowsReturnedUseQueryInstead) + Err(e) => Err(e) + } } - => Result {} [SqliteErr ErrCode Str, RowsReturnedUseQueryInstead] -execute! = |{ path, query: q, bindings }| - stmt = try(prepare!, { path, query: q }) - execute_prepared!({ stmt, bindings }) - -## Execute a prepared SQL statement that **doesn't return any rows** (like INSERT, UPDATE, DELETE). -## Use a function starting with `query_` if you expect rows to be returned. -## -## This is more efficient than [execute!] when running the same query multiple times -## as it reuses the prepared statement. -execute_prepared! : - { - stmt : Stmt, - bindings : List Binding, + + ## Execute a SQL query and decode exactly one row into a value. + query! = |{ path, query: q, bindings, row }| { + stmt = prepare!({ path, query: q })? + query_prepared!({ stmt, bindings, row }) } - => Result {} [SqliteErr ErrCode Str, RowsReturnedUseQueryInstead] -execute_prepared! = |{ stmt, bindings }| - try(bind!, stmt, bindings) - res = step!(stmt) - try(reset!, stmt) - when res is - Ok(Done) -> - Ok({}) - - Ok(Row) -> - Err(RowsReturnedUseQueryInstead) - - Err(e) -> - Err(e) - -## Execute a SQL query and decode exactly one row into a value. -## -## Example: -## ``` -## # count the number of rows in the `users` table -## count = Sqlite.query!({ -## path: db_path, -## query: "SELECT COUNT(*) as \"count\" FROM users;", -## bindings: [], -## row: Sqlite.u64("count"), -## })? -## ``` -query! : - { - path : Str, - query : Str, - bindings : List Binding, - row : SqlDecode a (RowCountErr err), + + ## Execute a prepared SQL query and decode exactly one row into a value. + query_prepared! = |{ stmt, bindings, row: decode }| { + bind!(stmt, bindings)? + res = decode_exactly_one_row!(stmt, decode) + reset!(stmt)? + res } - => Result a (SqlDecodeErr (RowCountErr err)) -query! = |{ path, query: q, bindings, row }| - stmt = try(prepare!, { path, query: q }) - query_prepared!({ stmt, bindings, row }) - -## Execute a prepared SQL query and decode exactly one row into a value. -## -## This is more efficient than [query!] when running the same query multiple times -## as it reuses the prepared statement. -query_prepared! : - { - stmt : Stmt, - bindings : List Binding, - row : SqlDecode a (RowCountErr err), + + ## Execute a SQL query and decode multiple rows into a list of values. + query_many! = |{ path, query: q, bindings, rows }| { + stmt = prepare!({ path, query: q })? + query_many_prepared!({ stmt, bindings, rows }) } - => Result a (SqlDecodeErr (RowCountErr err)) -query_prepared! = |{ stmt, bindings, row: decode }| - try(bind!, stmt, bindings) - res = decode_exactly_one_row!(stmt, decode) - try(reset!, stmt) - res - -## Execute a SQL query and decode multiple rows into a list of values. -## -## Example: -## ``` -## rows = Sqlite.query_many!({ -## path: "path/to/database.db", -## query: "SELECT * FROM todos;", -## bindings: [], -## rows: { Sqlite.decode_record <- -## id: Sqlite.i64("id"), -## task: Sqlite.str("task"), -## }, -## })? -## ``` -query_many! : - { - path : Str, - query : Str, - bindings : List Binding, - rows : SqlDecode a err, + + ## Execute a prepared SQL query and decode multiple rows into a list of values. + query_many_prepared! = |{ stmt, bindings, rows: decode }| { + bind!(stmt, bindings)? + res = decode_rows!(stmt, decode) + reset!(stmt)? + res } - => Result (List a) (SqlDecodeErr err) -query_many! = |{ path, query: q, bindings, rows }| - stmt = try(prepare!, { path, query: q }) - query_many_prepared!({ stmt, bindings, rows }) - -## Execute a prepared SQL query and decode multiple rows into a list of values. -## -## This is more efficient than [query_many!] when running the same query multiple times -## as it reuses the prepared statement. -query_many_prepared! : - { - stmt : Stmt, - bindings : List Binding, - rows : SqlDecode a err, + + # internal use only + decode_exactly_one_row! = |stmt, gen_decode| { + cols = columns!(stmt) + decode_row! = gen_decode(cols) + match step!(stmt)? { + Row => { + row = decode_row!(stmt)? + match step!(stmt)? { + Done => Ok(row) + Row => Err(TooManyRowsReturned) + } + } + Done => Err(NoRowsReturned) + } } - => Result (List a) (SqlDecodeErr err) -query_many_prepared! = |{ stmt, bindings, rows: decode }| - try(bind!, stmt, bindings) - res = decode_rows!(stmt, decode) - try(reset!, stmt) - res - -SqlDecodeErr err : [NoSuchField Str, SqliteErr ErrCode Str]err -SqlDecode a err := List Str -> (Stmt => Result a (SqlDecodeErr err)) - -## Decode a Sqlite row into a record by combining decoders. -## -## Example: -## ``` -## { Sqlite.decode_record <- -## id: Sqlite.i64("id"), -## task: Sqlite.str("task"), -## } -## ``` -decode_record : SqlDecode a err, SqlDecode b err, (a, b -> c) -> SqlDecode c err -decode_record = |@SqlDecode(gen_first), @SqlDecode(gen_second), mapper| - @SqlDecode( + + # internal use only + decode_rows! = |stmt, gen_decode| { + cols = columns!(stmt) + decode_row! = gen_decode(cols) + helper! = |out| + match step!(stmt)? { + Done => Ok(out) + Row => { + row = decode_row!(stmt)? + helper!(out.append(row)) + } + } + helper!([]) + } + + # ---- Row decoding combinators ---------------------------------------------- + + ## Decode a Sqlite row into a record by combining two decoders. + decode_record = |gen_first, gen_second, mapper| |cols| - decode_first! = gen_first(cols) - decode_second! = gen_second(cols) + |stmt| + match gen_first(cols)(stmt) { + Ok(first) => + match gen_second(cols)(stmt) { + Ok(second) => Ok(mapper(first, second)) + Err(e) => Err(e) + } + Err(e) => Err(e) + } + + ## Transform the output of a decoder by applying a function to the decoded value. + map_value = |gen_decode, mapper| + |cols| + |stmt| + match gen_decode(cols)(stmt) { + Ok(val) => Ok(mapper(val)) + Err(e) => Err(e) + } + ## Transform a decoder's output with a function returning a `Try`. + map_value_result = |gen_decode, mapper| + |cols| + |stmt| + match gen_decode(cols)(stmt) { + Ok(val) => mapper(val) + Err(e) => Err(e) + } + + # internal use only — look the named column's value up in the current row. + lookup_value! = |cols, stmt, name| + match cols.find_first_index(|x| x == name) { + Ok(index) => column_value!(stmt, index) + Err(NotFound) => Err(NoSuchField(name)) + } + + # ---- Leaf decoders --------------------------------------------------------- + + ## Decode a [Value] keeping it tagged. + tagged_value = |name| + |cols| + |stmt| lookup_value!(cols, stmt, name) + + ## Decode a column to a [Str]. + str = |name| + |cols| |stmt| - first = try(decode_first!, stmt) - second = try(decode_second!, stmt) - Ok(mapper(first, second)), - ) - -## Transform the output of a decoder by applying a function to the decoded value. -## -## Example: -## ``` -## Sqlite.i64("id") |> Sqlite.map_value(Num.to_str) -## ``` -map_value : SqlDecode a err, (a -> b) -> SqlDecode b err -map_value = |@SqlDecode(gen_decode), mapper| - @SqlDecode( + match lookup_value!(cols, stmt, name) { + Ok(String(s)) => Ok(s) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + ## Decode a column to a [List U8]. + bytes = |name| |cols| - decode! = gen_decode(cols) + |stmt| + match lookup_value!(cols, stmt, name) { + Ok(Bytes(b)) => Ok(b) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + ## Decode a column to a [I64]. + i64 = |name| int_decoder(name, |n| Ok(n)) + + ## Decode a column to a [I32]. + i32 = |name| int_decoder(name, |n| bounds_err(I64.to_i32_try(n))) + + ## Decode a column to a [I16]. + i16 = |name| int_decoder(name, |n| bounds_err(I64.to_i16_try(n))) + + ## Decode a column to a [I8]. + i8 = |name| int_decoder(name, |n| bounds_err(I64.to_i8_try(n))) + + ## Decode a column to a [U64]. + u64 = |name| int_decoder(name, |n| bounds_err(I64.to_u64_try(n))) + + ## Decode a column to a [U32]. + u32 = |name| int_decoder(name, |n| bounds_err(I64.to_u32_try(n))) + ## Decode a column to a [U16]. + u16 = |name| int_decoder(name, |n| bounds_err(I64.to_u16_try(n))) + + ## Decode a column to a [U8]. + u8 = |name| int_decoder(name, |n| bounds_err(I64.to_u8_try(n))) + + ## Decode a column to a [F64]. + f64 = |name| real_decoder(name, |r| Ok(r)) + + # Nullable decoders return `NotNull(value)` for a present value, or `Null` when + # the column holds SQL NULL. Useful for nullable columns. + + ## Decode a nullable column to `[NotNull(Str), Null]`. + nullable_str = |name| + |cols| |stmt| - val = try(decode!, stmt) - Ok(mapper(val)), - ) - -## Transform the output of a decoder by applying a function (that returns a Result) to the decoded value. -## The Result is converted to SqlDecode. -## -## Example: -## ``` -## decode_status : Str -> Result OnlineStatus UnknownStatusErr -## decode_status = |status_str| -## when status_str is -## "online" -> Ok(Online) -## "offline" -> Ok(Offline) -## _ -> Err(UnknownStatus("${status_str}")) -## -## Sqlite.str("status") |> Sqlite.map_value_result(decode_status) -## ``` -map_value_result : SqlDecode a err, (a -> Result c (SqlDecodeErr err)) -> SqlDecode c err -map_value_result = |@SqlDecode(gen_decode), mapper| - @SqlDecode( + match lookup_value!(cols, stmt, name) { + Ok(String(s)) => Ok(NotNull(s)) + Ok(Null) => Ok(Null) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + ## Decode a nullable column to `[NotNull(List(U8)), Null]`. + nullable_bytes = |name| |cols| - decode! = gen_decode(cols) + |stmt| + match lookup_value!(cols, stmt, name) { + Ok(Bytes(b)) => Ok(NotNull(b)) + Ok(Null) => Ok(Null) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + ## Decode a nullable column to `[NotNull(I64), Null]`. + nullable_i64 = |name| nullable_int_decoder(name, |n| Ok(n)) + + ## Decode a nullable column to `[NotNull(I32), Null]`. + nullable_i32 = |name| nullable_int_decoder(name, |n| bounds_err(I64.to_i32_try(n))) + + ## Decode a nullable column to `[NotNull(I16), Null]`. + nullable_i16 = |name| nullable_int_decoder(name, |n| bounds_err(I64.to_i16_try(n))) + + ## Decode a nullable column to `[NotNull(I8), Null]`. + nullable_i8 = |name| nullable_int_decoder(name, |n| bounds_err(I64.to_i8_try(n))) + + ## Decode a nullable column to `[NotNull(U64), Null]`. + nullable_u64 = |name| nullable_int_decoder(name, |n| bounds_err(I64.to_u64_try(n))) + + ## Decode a nullable column to `[NotNull(U32), Null]`. + nullable_u32 = |name| nullable_int_decoder(name, |n| bounds_err(I64.to_u32_try(n))) + + ## Decode a nullable column to `[NotNull(U16), Null]`. + nullable_u16 = |name| nullable_int_decoder(name, |n| bounds_err(I64.to_u16_try(n))) + ## Decode a nullable column to `[NotNull(U8), Null]`. + nullable_u8 = |name| nullable_int_decoder(name, |n| bounds_err(I64.to_u8_try(n))) + + ## Decode a nullable column to `[NotNull(F64), Null]`. + nullable_f64 = |name| nullable_real_decoder(name, |r| Ok(r)) + + # internal use only + nullable_int_decoder = |name, cast| + |cols| + |stmt| + match lookup_value!(cols, stmt, name) { + Ok(Integer(n)) => + match cast(n) { + Ok(v) => Ok(NotNull(v)) + Err(e) => Err(e) + } + Ok(Null) => Ok(Null) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + # internal use only + nullable_real_decoder = |name, cast| + |cols| + |stmt| + match lookup_value!(cols, stmt, name) { + Ok(Real(r)) => + match cast(r) { + Ok(v) => Ok(NotNull(v)) + Err(e) => Err(e) + } + Ok(Null) => Ok(Null) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + # internal use only + int_decoder = |name, cast| + |cols| + |stmt| + match lookup_value!(cols, stmt, name) { + Ok(Integer(n)) => cast(n) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + # internal use only + real_decoder = |name, cast| + |cols| |stmt| - val = try(decode!, stmt) - mapper(val), - ) - -RowCountErr err : [NoRowsReturned, TooManyRowsReturned]err - -# internal use only -decode_exactly_one_row! : Stmt, SqlDecode a (RowCountErr err) => Result a (SqlDecodeErr (RowCountErr err)) -decode_exactly_one_row! = |stmt, @SqlDecode(gen_decode)| - cols = columns!(stmt) - decode_row! = gen_decode(cols) - - when try(step!, stmt) is - Row -> - row = try(decode_row!, stmt) - when try(step!, stmt) is - Done -> - Ok(row) - - Row -> - Err(TooManyRowsReturned) - - Done -> - Err(NoRowsReturned) - -# internal use only -decode_rows! : Stmt, SqlDecode a err => Result (List a) (SqlDecodeErr err) -decode_rows! = |stmt, @SqlDecode(gen_decode)| - cols = columns!(stmt) - decode_row! = gen_decode(cols) - - helper! = |out| - when try(step!, stmt) is - Done -> - Ok(out) - - Row -> - row = try(decode_row!, stmt) - - List.append(out, row) - |> helper! - - helper!([]) - -# internal use only -decoder : (Value -> Result a (SqlDecodeErr err)) -> (Str -> SqlDecode a err) -decoder = |fn| - |name| - @SqlDecode( - |cols| - - found = List.find_first_index(cols, |x| x == name) - when found is - Ok(index) -> - |stmt| - try(column_value!, stmt, index) - |> fn - - Err(NotFound) -> - |_| - Err(NoSuchField(name)), - ) - -## Decode a [Value] keeping it tagged. This is useful when data could be many possible types. -## -## For example here we build a decoder that decodes the rows into a list of records with `id` and `mixed_data` fields: -## ``` -## rows = Sqlite.query_many!({ -## path: "path/to/database.db", -## query: "SELECT id, mix_data FROM users;", -## bindings: [], -## rows: { Sqlite.decode_record <- -## id: Sqlite.i64("id"), -## mix_data: Sqlite.tagged_value("mixed_data"), -## }, -## })? -## ``` -tagged_value : Str -> SqlDecode Value [] -tagged_value = decoder( - |val| - Ok(val), -) - -to_unexpected_type_err = |val| + match lookup_value!(cols, stmt, name) { + Ok(Real(r)) => cast(r) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + ## Convert a [ErrCode] to a pretty string for display purposes. + errcode_to_str = |code| + match code { + Error => "Error: Sql error or missing database" + Internal => "Internal: Internal logic error in Sqlite" + Perm => "Perm: Access permission denied" + Abort => "Abort: Callback routine requested an abort" + Busy => "Busy: The database file is locked" + Locked => "Locked: A table in the database is locked" + NoMem => "NoMem: A malloc() failed" + ReadOnly => "ReadOnly: Attempt to write a readonly database" + Interrupt => "Interrupt: Operation terminated by sqlite3_interrupt(" + IOErr => "IOErr: Some kind of disk I/O error occurred" + Corrupt => "Corrupt: The database disk image is malformed" + NotFound => "NotFound: Unknown opcode in sqlite3_file_control()" + Full => "Full: Insertion failed because database is full" + CanNotOpen => "CanNotOpen: Unable to open the database file" + Protocol => "Protocol: Database lock protocol error" + Empty => "Empty: Database is empty" + Schema => "Schema: The database schema changed" + TooBig => "TooBig: String or BLOB exceeds size limit" + Constraint => "Constraint: Abort due to constraint violation" + Mismatch => "Mismatch: Data type mismatch" + Misuse => "Misuse: Library used incorrectly" + NoLFS => "NoLFS: Uses OS features not supported on host" + AuthDenied => "AuthDenied: Authorization denied" + Format => "Format: Auxiliary database format error" + OutOfRange => "OutOfRange: 2nd parameter to sqlite3_bind out of range" + NotADatabase => "NotADatabase: File opened that is not a database file" + Notice => "Notice: Notifications from sqlite3_log()" + Warning => "Warning: Warnings from sqlite3_log()" + Row => "Row: sqlite3_step() has another row ready" + Done => "Done: sqlite3_step() has finished executing" + Unknown(c) => "Unknown: error code ${I64.to_str(c)} not known" + } +} + +# ---- internal helpers (module-private) ----------------------------------------- + +to_unexpected_type_err = |val| { type = - when val is - Integer(_) -> Integer - Real(_) -> Real - String(_) -> String - Bytes(_) -> Bytes - Null -> Null + match val { + Integer(_) => Integer + Real(_) => Real + String(_) => String + Bytes(_) => Bytes + Null => Null + } Err(UnexpectedType(type)) +} -UnexpectedTypeErr : [UnexpectedType [Integer, Real, String, Bytes, Null]] - -## Decode a [Value] to a [Str]. -## -## For example here we build a decoder that decodes the rows into a list of records with `id` and `name` fields: -## ``` -## rows = Sqlite.query_many!({ -## path: "path/to/database.db", -## query: "SELECT id, name FROM users;", -## bindings: [], -## rows: { Sqlite.decode_record <- -## id: Sqlite.i64("id"), -## task: Sqlite.str("name"), -## }, -## })? -## ``` -str : Str -> SqlDecode Str UnexpectedTypeErr -str = decoder( - |val| - when val is - String(s) -> Ok(s) - _ -> to_unexpected_type_err(val), -) - -## Decode a [Value] to a [List U8]. -bytes : Str -> SqlDecode (List U8) UnexpectedTypeErr -bytes = decoder( - |val| - when val is - Bytes(b) -> Ok(b) - _ -> to_unexpected_type_err(val), -) - -# internal use only -int_decoder : (I64 -> Result a err) -> (Str -> SqlDecode a [FailedToDecodeInteger err]UnexpectedTypeErr) -int_decoder = |cast| - decoder( - |val| - when val is - Integer(i) -> cast(i) |> Result.map_err(FailedToDecodeInteger) - _ -> to_unexpected_type_err(val), - ) - -# internal use only -real_decoder : (F64 -> Result a err) -> (Str -> SqlDecode a [FailedToDecodeReal err]UnexpectedTypeErr) -real_decoder = |cast| - decoder( - |val| - when val is - Real(r) -> cast(r) |> Result.map_err(FailedToDecodeReal) - _ -> to_unexpected_type_err(val), - ) - -## Decode a [Value] to a [I64]. -## -## For example here we build a decoder that decodes the rows into a list of records with `id` and `name` fields: -## ``` -## rows = Sqlite.query_many!({ -## path: "path/to/database.db", -## query: "SELECT id, name FROM users;", -## bindings: [], -## rows: { Sqlite.decode_record <- -## id: Sqlite.i64("id"), -## task: Sqlite.str("name"), -## }, -## })? -## ``` -i64 : Str -> SqlDecode I64 [FailedToDecodeInteger []]UnexpectedTypeErr -i64 = int_decoder(Ok) - -## Decode a [Value] to a [I32]. -i32 : Str -> SqlDecode I32 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -i32 = int_decoder(Num.to_i32_checked) - -## Decode a [Value] to a [I16]. -i16 : Str -> SqlDecode I16 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -i16 = int_decoder(Num.to_i16_checked) - -## Decode a [Value] to a [I8]. -i8 : Str -> SqlDecode I8 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -i8 = int_decoder(Num.to_i8_checked) - -## Decode a [Value] to a [U64]. -u64 : Str -> SqlDecode U64 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -u64 = int_decoder(Num.to_u64_checked) - -## Decode a [Value] to a [U32]. -u32 : Str -> SqlDecode U32 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -u32 = int_decoder(Num.to_u32_checked) - -## Decode a [Value] to a [U16]. -u16 : Str -> SqlDecode U16 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -u16 = int_decoder(Num.to_u16_checked) - -## Decode a [Value] to a [U8]. -u8 : Str -> SqlDecode U8 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -u8 = int_decoder(Num.to_u8_checked) - -## Decode a [Value] to a [F64]. -f64 : Str -> SqlDecode F64 [FailedToDecodeReal []]UnexpectedTypeErr -f64 = real_decoder(Ok) - -## Decode a [Value] to a [F32]. -f32 : Str -> SqlDecode F32 [FailedToDecodeReal []]UnexpectedTypeErr -f32 = real_decoder(|x| Num.to_f32(x) |> Ok) - -# TODO: Mising Num.to_dec and Num.to_dec_checked -# dec = real_sql_decoder Ok - -# These are the same decoders as above but Nullable. -# If the sqlite field is `Null`, they will return `Null`. - -## Represents a nullable value that can be stored in a Sqlite database. -Nullable a : [NotNull a, Null] - -## Decode a [Value] to a [Nullable Str]. -nullable_str : Str -> SqlDecode (Nullable Str) UnexpectedTypeErr -nullable_str = decoder( - |val| - when val is - String(s) -> Ok(NotNull(s)) - Null -> Ok(Null) - _ -> to_unexpected_type_err(val), -) - -## Decode a [Value] to a [Nullable (List U8)]. -nullable_bytes : Str -> SqlDecode (Nullable (List U8)) UnexpectedTypeErr -nullable_bytes = decoder( - |val| - when val is - Bytes(b) -> Ok(NotNull(b)) - Null -> Ok(Null) - _ -> to_unexpected_type_err(val), -) - -# internal use only -nullable_int_decoder : (I64 -> Result a err) -> (Str -> SqlDecode (Nullable a) [FailedToDecodeInteger err]UnexpectedTypeErr) -nullable_int_decoder = |cast| - decoder( - |val| - when val is - Integer(i) -> cast(i) |> Result.map_ok(NotNull) |> Result.map_err(FailedToDecodeInteger) - Null -> Ok(Null) - _ -> to_unexpected_type_err(val), - ) - -# internal use only -nullable_real_decoder : (F64 -> Result a err) -> (Str -> SqlDecode (Nullable a) [FailedToDecodeReal err]UnexpectedTypeErr) -nullable_real_decoder = |cast| - decoder( - |val| - when val is - Real(r) -> cast(r) |> Result.map_ok(NotNull) |> Result.map_err(FailedToDecodeReal) - Null -> Ok(Null) - _ -> to_unexpected_type_err(val), - ) - -## Decode a [Value] to a [Nullable I64]. -nullable_i64 : Str -> SqlDecode (Nullable I64) [FailedToDecodeInteger []]UnexpectedTypeErr -nullable_i64 = nullable_int_decoder(Ok) - -## Decode a [Value] to a [Nullable I32]. -nullable_i32 : Str -> SqlDecode (Nullable I32) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_i32 = nullable_int_decoder(Num.to_i32_checked) - -## Decode a [Value] to a [Nullable I16]. -nullable_i16 : Str -> SqlDecode (Nullable I16) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_i16 = nullable_int_decoder(Num.to_i16_checked) - -## Decode a [Value] to a [Nullable I8]. -nullable_i8 : Str -> SqlDecode (Nullable I8) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_i8 = nullable_int_decoder(Num.to_i8_checked) - -## Decode a [Value] to a [Nullable U64]. -nullable_u64 : Str -> SqlDecode (Nullable U64) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_u64 = nullable_int_decoder(Num.to_u64_checked) - -## Decode a [Value] to a [Nullable U32]. -nullable_u32 : Str -> SqlDecode (Nullable U32) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_u32 = nullable_int_decoder(Num.to_u32_checked) - -## Decode a [Value] to a [Nullable U16]. -nullable_u16 : Str -> SqlDecode (Nullable U16) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_u16 = nullable_int_decoder(Num.to_u16_checked) - -## Decode a [Value] to a [Nullable U8]. -nullable_u8 : Str -> SqlDecode (Nullable U8) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_u8 = nullable_int_decoder(Num.to_u8_checked) - -## Decode a [Value] to a [Nullable F64]. -nullable_f64 : Str -> SqlDecode (Nullable F64) [FailedToDecodeReal []]UnexpectedTypeErr -nullable_f64 = nullable_real_decoder(Ok) - -## Decode a [Value] to a [Nullable F32]. -nullable_f32 : Str -> SqlDecode (Nullable F32) [FailedToDecodeReal []]UnexpectedTypeErr -nullable_f32 = nullable_real_decoder(|x| Num.to_f32(x) |> Ok) - -# TODO: Mising Num.to_dec and Num.to_dec_checked -# nullable_dec = nullable_real_decoder Ok - -# internal use only -internal_to_external_error : InternalSqlite.SqliteError -> [SqliteErr ErrCode Str] -internal_to_external_error = |{ code, message }| - SqliteErr(code_from_i64(code), message) - -# internal use only -code_from_i64 : I64 -> ErrCode +bounds_err = |result| + match result { + Ok(v) => Ok(v) + Err(_) => Err(IntOutOfBounds) + } + +code_from_i64 : I64 -> Sqlite.ErrCode code_from_i64 = |code| - if code == 1 or code == 0 then - Error - else if code == 2 then - Internal - else if code == 3 then - Perm - else if code == 4 then - Abort - else if code == 5 then - Busy - else if code == 6 then - Locked - else if code == 7 then - NoMem - else if code == 8 then - ReadOnly - else if code == 9 then - Interrupt - else if code == 10 then - IOErr - else if code == 11 then - Corrupt - else if code == 12 then - NotFound - else if code == 13 then - Full - else if code == 14 then - CanNotOpen - else if code == 15 then - Protocol - else if code == 16 then - Empty - else if code == 17 then - Schema - else if code == 18 then - TooBig - else if code == 19 then - Constraint - else if code == 20 then - Mismatch - else if code == 21 then - Misuse - else if code == 22 then - NoLFS - else if code == 23 then - AuthDenied - else if code == 24 then - Format - else if code == 25 then - OutOfRange - else if code == 26 then - NotADatabase - else if code == 27 then - Notice - else if code == 28 then - Warning - else if code == 100 then - Row - else if code == 101 then - Done - else - Unknown(code) - -## Convert a [ErrCode] to a pretty string for display purposes. -errcode_to_str : ErrCode -> Str -errcode_to_str = |code| - when code is - Error -> "Error: Sql error or missing database" - Internal -> "Internal: Internal logic error in Sqlite" - Perm -> "Perm: Access permission denied" - Abort -> "Abort: Callback routine requested an abort" - Busy -> "Busy: The database file is locked" - Locked -> "Locked: A table in the database is locked" - NoMem -> "NoMem: A malloc() failed" - ReadOnly -> "ReadOnly: Attempt to write a readonly database" - Interrupt -> "Interrupt: Operation terminated by sqlite3_interrupt(" - IOErr -> "IOErr: Some kind of disk I/O error occurred" - Corrupt -> "Corrupt: The database disk image is malformed" - NotFound -> "NotFound: Unknown opcode in sqlite3_file_control()" - Full -> "Full: Insertion failed because database is full" - CanNotOpen -> "CanNotOpen: Unable to open the database file" - Protocol -> "Protocol: Database lock protocol error" - Empty -> "Empty: Database is empty" - Schema -> "Schema: The database schema changed" - TooBig -> "TooBig: String or BLOB exceeds size limit" - Constraint -> "Constraint: Abort due to constraint violation" - Mismatch -> "Mismatch: Data type mismatch" - Misuse -> "Misuse: Library used incorrectly" - NoLFS -> "NoLFS: Uses OS features not supported on host" - AuthDenied -> "AuthDenied: Authorization denied" - Format -> "Format: Auxiliary database format error" - OutOfRange -> "OutOfRange: 2nd parameter to sqlite3_bind out of range" - NotADatabase -> "NotADatabase: File opened that is not a database file" - Notice -> "Notice: Notifications from sqlite3_log()" - Warning -> "Warning: Warnings from sqlite3_log()" - Row -> "Row: sqlite3_step() has another row ready" - Done -> "Done: sqlite3_step() has finished executing" - Unknown(c) -> "Unknown: error code ${Num.to_str(c)} not known" + match code { + 0 => Error + 1 => Error + 2 => Internal + 3 => Perm + 4 => Abort + 5 => Busy + 6 => Locked + 7 => NoMem + 8 => ReadOnly + 9 => Interrupt + 10 => IOErr + 11 => Corrupt + 12 => NotFound + 13 => Full + 14 => CanNotOpen + 15 => Protocol + 16 => Empty + 17 => Schema + 18 => TooBig + 19 => Constraint + 20 => Mismatch + 21 => Misuse + 22 => NoLFS + 23 => AuthDenied + 24 => Format + 25 => OutOfRange + 26 => NotADatabase + 27 => Notice + 28 => Warning + 100 => Row + 101 => Done + other => Unknown(other) + } diff --git a/platform/Stderr.roc b/platform/Stderr.roc index 0c71fa15..a823020a 100644 --- a/platform/Stderr.roc +++ b/platform/Stderr.roc @@ -1,72 +1,23 @@ -module [ - IOErr, - line!, - write!, - write_bytes!, -] +import IOErr exposing [IOErr] -import Host -import InternalIOErr +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 => Try({}, [StderrErr(IOErr), ..]) -## **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, -] + ## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). + ## + ## Most terminals will not actually display strings that are written to them until they receive a newline, + ## 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), ..]) -handle_err : InternalIOErr.IOErrFromHost -> [StderrErr IOErr] -handle_err = |{ tag, msg }| - when tag is - NotFound -> StderrErr(NotFound) - PermissionDenied -> StderrErr(PermissionDenied) - BrokenPipe -> StderrErr(BrokenPipe) - AlreadyExists -> StderrErr(AlreadyExists) - Interrupted -> StderrErr(Interrupted) - Unsupported -> StderrErr(Unsupported) - OutOfMemory -> StderrErr(OutOfMemory) - Other | EndOfFile -> StderrErr(Other(msg)) - -## 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 => Result {} [StderrErr IOErr] -line! = |str| - Host.stderr_line!(str) - |> Result.map_err(handle_err) - -## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). -## -## Most terminals will not actually display strings that are written to them until they receive a newline, -## 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 => Result {} [StderrErr IOErr] -write! = |str| - Host.stderr_write!(str) - |> Result.map_err(handle_err) - -write_bytes! : List U8 => Result {} [StderrErr IOErr] -write_bytes! = |bytes| - Host.stderr_write_bytes!(bytes) - |> Result.map_err(handle_err) \ No newline at end of file + ## 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 90ad5be6..46da4e33 100644 --- a/platform/Stdin.roc +++ b/platform/Stdin.roc @@ -1,89 +1,23 @@ -module [ - IOErr, - line!, - bytes!, - read_to_end!, -] +import IOErr exposing [IOErr] -import Host -import InternalIOErr +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! : {} => Try(Str, [EndOfFile, StdinErr(IOErr), ..]) -## **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, -] + ## 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), ..]) -handle_err : InternalIOErr.IOErrFromHost -> [EndOfFile, StdinErr IOErr] -handle_err = |{ tag, msg }| - when tag is - NotFound -> StdinErr(NotFound) - PermissionDenied -> StdinErr(PermissionDenied) - BrokenPipe -> StdinErr(BrokenPipe) - AlreadyExists -> StdinErr(AlreadyExists) - Interrupted -> StdinErr(Interrupted) - Unsupported -> StdinErr(Unsupported) - OutOfMemory -> StdinErr(OutOfMemory) - EndOfFile -> EndOfFile - Other -> StdinErr(Other(msg)) - -## 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 -## programming 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! : {} => Result Str [EndOfFile, StdinErr IOErr] -line! = |{}| - Host.stdin_line!({}) - |> Result.map_err(handle_err) - -## 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! : {} => Result (List U8) [EndOfFile, StdinErr IOErr] -bytes! = |{}| - Host.stdin_bytes!({}) - |> Result.map_err(handle_err) - -## 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! : {} => Result (List U8) [StdinErr IOErr] -read_to_end! = |{}| - Host.stdin_read_to_end!({}) - |> Result.map_err( - |{ tag, msg }| - when tag is - NotFound -> StdinErr(NotFound) - PermissionDenied -> StdinErr(PermissionDenied) - BrokenPipe -> StdinErr(BrokenPipe) - AlreadyExists -> StdinErr(AlreadyExists) - Interrupted -> StdinErr(Interrupted) - Unsupported -> StdinErr(Unsupported) - OutOfMemory -> StdinErr(OutOfMemory) - EndOfFile -> crash("unreachable, reading to EOF") - Other -> StdinErr(Other(msg)), - ) + ## 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 569299d2..83ca3b52 100644 --- a/platform/Stdout.roc +++ b/platform/Stdout.roc @@ -1,73 +1,23 @@ -module [ - IOErr, - line!, - write!, - write_bytes!, -] +import IOErr exposing [IOErr] -import Host -import InternalIOErr +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 => Try({}, [StdoutErr(IOErr), ..]) -## **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, -] + ## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). + ## + ## Note that many terminals will not actually display strings that are written to them until they receive a newline, + ## 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), ..]) -handle_err : InternalIOErr.IOErrFromHost -> [StdoutErr IOErr] -handle_err = |{ tag, msg }| - when tag is - NotFound -> StdoutErr(NotFound) - PermissionDenied -> StdoutErr(PermissionDenied) - BrokenPipe -> StdoutErr(BrokenPipe) - AlreadyExists -> StdoutErr(AlreadyExists) - Interrupted -> StdoutErr(Interrupted) - Unsupported -> StdoutErr(Unsupported) - OutOfMemory -> StdoutErr(OutOfMemory) - Other | EndOfFile -> StdoutErr(Other(msg)) - -## 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 => Result {} [StdoutErr IOErr] -line! = |str| - Host.stdout_line!(str) - |> Result.map_err(handle_err) - -## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). -## -## Note that many terminals will not actually display strings that are written to them until they receive a newline, -## 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 => Result {} [StdoutErr IOErr] -write! = |str| - Host.stdout_write!(str) - |> Result.map_err(handle_err) - -write_bytes! : List U8 => Result {} [StdoutErr IOErr] -write_bytes! = |bytes| - Host.stdout_write_bytes!(bytes) - |> Result.map_err(handle_err) + ## 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/Tcp.roc b/platform/Tcp.roc index 724126d1..221d521f 100644 --- a/platform/Tcp.roc +++ b/platform/Tcp.roc @@ -1,225 +1,177 @@ -module [ - Stream, - ConnectErr, - StreamErr, - connect!, - read_up_to!, - read_exactly!, - read_until!, - read_line!, - write!, - write_utf8!, - connect_err_to_str, - stream_err_to_str, -] - -import Host - -unexpected_eof_error_message = "UnexpectedEof" - -## Represents a TCP stream. -Stream := Host.TcpStream - -## Represents errors that can occur when connecting to a remote host. -ConnectErr a : [ - PermissionDenied, - AddrInUse, - AddrNotAvailable, - ConnectionRefused, - Interrupted, - TimedOut, - Unsupported, - Unrecognized Str, -]a - -parse_connect_err : Str -> ConnectErr _ +Tcp := [].{ + ## Represents a TCP stream. + ## + ## The connection is automatically closed when the last reference to the + ## stream is dropped. This is an opaque `Box(U64)` handle into a host-side + ## `BufReader`. + Stream :: Box(U64) + + # ---- Host functions (the FFI boundary) ------------------------------------- + # Errors are carried across as raw `Str` and parsed into tag unions below. + + host_connect! : Str, U16 => Try(Stream, Str) + host_read_up_to! : Stream, U64 => Try(List(U8), Str) + host_read_exactly! : Stream, U64 => Try(List(U8), Str) + host_read_until! : Stream, U8 => Try(List(U8), Str) + host_write! : Stream, List(U8) => Try({}, Str) + + ## Represents errors that can occur when connecting to a remote host. + ConnectErr : [ + PermissionDenied, + AddrInUse, + AddrNotAvailable, + ConnectionRefused, + Interrupted, + TimedOut, + Unsupported, + Unrecognized(Str), + ] + + ## Represents errors that can occur when performing an effect with a [Stream]. + StreamErr : [ + StreamNotFound, + PermissionDenied, + ConnectionRefused, + ConnectionReset, + Interrupted, + OutOfMemory, + BrokenPipe, + Unrecognized(Str), + ] + + ## Opens a TCP connection to a remote host. + ## + ## ```roc + ## # Connect to localhost:8080 + ## stream = Tcp.connect!("localhost", 8080)? + ## ``` + ## + ## Valid hostnames look like `127.0.0.1`, `::1`, `localhost`, or `roc-lang.org`. + connect! = |host, port| + Tcp.host_connect!(host, port).map_err(parse_connect_err) + + ## Read up to a number of bytes from the TCP stream. + ## + ## ```roc + ## received_bytes = Tcp.read_up_to!(stream, 64)? + ## ``` + ## + ## > To read an exact number of bytes or fail, use [Tcp.read_exactly!] instead. + read_up_to! = |stream, bytes_to_read| + Tcp.host_read_up_to!(stream, bytes_to_read) + .map_err(|err| TcpReadErr(parse_stream_err(err))) + + ## Read an exact number of bytes or fail. + ## + ## ```roc + ## bytes = Tcp.read_exactly!(stream, 64)? + ## ``` + ## + ## `TcpUnexpectedEOF` is returned if the stream ends before the specified + ## number of bytes is reached. + read_exactly! = |stream, bytes_to_read| + match Tcp.host_read_exactly!(stream, bytes_to_read) { + Ok(bytes) => Ok(bytes) + Err("UnexpectedEof") => Err(TcpUnexpectedEOF) + Err(err) => Err(TcpReadErr(parse_stream_err(err))) + } + + ## Read until a delimiter or EOF is reached. + ## + ## ```roc + ## # Read until null terminator + ## bytes = Tcp.read_until!(stream, 0)? + ## ``` + ## + ## If found, the delimiter is included as the last byte. + read_until! = |stream, byte| + Tcp.host_read_until!(stream, byte) + .map_err(|err| TcpReadErr(parse_stream_err(err))) + + ## Read until a newline (`\n`, byte 10) or EOF is reached, decoded as a [Str]. + ## + ## ```roc + ## line_str = Tcp.read_line!(stream)? + ## ``` + ## + ## If found, the newline is included as the last character in the [Str]. + read_line! = |stream| + # NB: use `match` rather than `?` here — `read_until!` yields a single- + # variant error union and `?` on that currently miscompiles (roc#9826). + match read_until!(stream, 10) { + Ok(bytes) => Str.from_utf8(bytes).map_err(|err| TcpReadBadUtf8(err)) + Err(err) => Err(err) + } + + ## Writes bytes to a TCP stream. + ## + ## ```roc + ## # Writes the bytes 1, 2, 3 + ## Tcp.write!(stream, [1, 2, 3])? + ## ``` + ## + ## > To write a [Str], use [Tcp.write_utf8!] instead. + write! = |stream, bytes| + Tcp.host_write!(stream, bytes) + .map_err(|err| TcpWriteErr(parse_stream_err(err))) + + ## Writes a [Str] to a TCP stream, encoded as UTF-8. + ## + ## ```roc + ## Tcp.write_utf8!(stream, "Hi from Roc!")? + ## ``` + write_utf8! = |stream, str| + write!(stream, Str.to_utf8(str)) + + ## Convert a [ConnectErr] to a [Str] you can print. + connect_err_to_str = |err| + match err { + PermissionDenied => "PermissionDenied" + AddrInUse => "AddrInUse" + AddrNotAvailable => "AddrNotAvailable" + ConnectionRefused => "ConnectionRefused" + Interrupted => "Interrupted" + TimedOut => "TimedOut" + Unsupported => "Unsupported" + Unrecognized(message) => "Unrecognized Error: ${message}" + } + + ## Convert a [StreamErr] to a [Str] you can print. + stream_err_to_str = |err| + match err { + StreamNotFound => "StreamNotFound" + PermissionDenied => "PermissionDenied" + ConnectionRefused => "ConnectionRefused" + ConnectionReset => "ConnectionReset" + Interrupted => "Interrupted" + OutOfMemory => "OutOfMemory" + BrokenPipe => "BrokenPipe" + Unrecognized(message) => "Unrecognized Error: ${message}" + } +} + +# ---- internal helpers (module-private) ----------------------------------------- + parse_connect_err = |err| - when err is - "ErrorKind::PermissionDenied" -> PermissionDenied - "ErrorKind::AddrInUse" -> AddrInUse - "ErrorKind::AddrNotAvailable" -> AddrNotAvailable - "ErrorKind::ConnectionRefused" -> ConnectionRefused - "ErrorKind::Interrupted" -> Interrupted - "ErrorKind::TimedOut" -> TimedOut - "ErrorKind::Unsupported" -> Unsupported - other -> Unrecognized(other) - -## Represents errors that can occur when performing an effect with a [Stream]. -StreamErr : [ - StreamNotFound, - PermissionDenied, - ConnectionRefused, - ConnectionReset, - Interrupted, - OutOfMemory, - BrokenPipe, - Unrecognized Str, -] - -parse_stream_err : Str -> StreamErr + match err { + "ErrorKind::PermissionDenied" => PermissionDenied + "ErrorKind::AddrInUse" => AddrInUse + "ErrorKind::AddrNotAvailable" => AddrNotAvailable + "ErrorKind::ConnectionRefused" => ConnectionRefused + "ErrorKind::Interrupted" => Interrupted + "ErrorKind::TimedOut" => TimedOut + "ErrorKind::Unsupported" => Unsupported + other => Unrecognized(other) + } + parse_stream_err = |err| - when err is - "StreamNotFound" -> StreamNotFound - "ErrorKind::PermissionDenied" -> PermissionDenied - "ErrorKind::ConnectionRefused" -> ConnectionRefused - "ErrorKind::ConnectionReset" -> ConnectionReset - "ErrorKind::Interrupted" -> Interrupted - "ErrorKind::OutOfMemory" -> OutOfMemory - "ErrorKind::BrokenPipe" -> BrokenPipe - other -> Unrecognized(other) - -## Opens a TCP connection to a remote host. -## -## ``` -## # Connect to localhost:8080 -## stream = Tcp.connect!("localhost", 8080)? -## ``` -## -## The connection is automatically closed when the last reference to the stream is dropped. -## Examples of -## valid hostnames: -## - `127.0.0.1` -## - `::1` -## - `localhost` -## - `roc-lang.org` -## -connect! : Str, U16 => Result Stream (ConnectErr _) -connect! = |host, port| - Host.tcp_connect!(host, port) - |> Result.map_ok(@Stream) - |> Result.map_err(parse_connect_err) - -## Read up to a number of bytes from the TCP stream. -## -## ``` -## # Read up to 64 bytes from the stream -## received_bytes = Tcp.read_up_to!(stream, 64)? -## ``` -## -## > To read an exact number of bytes or fail, you can use [Tcp.read_exactly!] instead. -read_up_to! : Stream, U64 => Result (List U8) [TcpReadErr StreamErr] -read_up_to! = |@Stream(stream), bytes_to_read| - Host.tcp_read_up_to!(stream, bytes_to_read) - |> Result.map_err(|err| TcpReadErr(parse_stream_err(err))) - -## Read an exact number of bytes or fail. -## -## ``` -## bytes = Tcp.read_exactly!(stream, 64)? -## ``` -## -## `TcpUnexpectedEOF` is returned if the stream ends before the specfied number of bytes is reached. -## -read_exactly! : Stream, U64 => Result (List U8) [TcpReadErr StreamErr, TcpUnexpectedEOF] -read_exactly! = |@Stream(stream), bytes_to_read| - Host.tcp_read_exactly!(stream, bytes_to_read) - |> Result.map_err( - |err| - if err == unexpected_eof_error_message then - TcpUnexpectedEOF - else - TcpReadErr(parse_stream_err(err)), - ) - -## Read until a delimiter or EOF is reached. -## -## ``` -## # Read until null terminator -## bytes = Tcp.read_until!(stream, 0)? -## ``` -## -## If found, the delimiter is included as the last byte. -## -## > To read until a newline is found, you can use [Tcp.read_line!] which -## conveniently decodes to a [Str]. -read_until! : Stream, U8 => Result (List U8) [TcpReadErr StreamErr] -read_until! = |@Stream(stream), byte| - Host.tcp_read_until!(stream, byte) - |> Result.map_err(|err| TcpReadErr(parse_stream_err(err))) - -## Read until a newline or EOF is reached. -## -## ``` -## # Read a line and then print it to `stdout` -## line_str = Tcp.read_line!(stream)? -## Stdout.line(line_str)? -## ``` -## -## If found, the newline is included as the last character in the [Str]. -## -read_line! : Stream => Result Str [TcpReadErr StreamErr, TcpReadBadUtf8 _] -read_line! = |stream| - bytes = read_until!(stream, '\n')? - - Str.from_utf8(bytes) - |> Result.map_err(TcpReadBadUtf8) - -## Writes bytes to a TCP stream. -## -## ``` -## # Writes the bytes 1, 2, 3 -## Tcp.write!(stream, [1, 2, 3])? -## ``` -## -## > To write a [Str], you can use [Tcp.write_utf8!] instead. -write! : Stream, List U8 => Result {} [TcpWriteErr StreamErr] -write! = |@Stream(stream), bytes| - Host.tcp_write!(stream, bytes) - |> Result.map_err(|err| TcpWriteErr(parse_stream_err(err))) - -## Writes a [Str] to a TCP stream, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). -## -## ``` -## # Write "Hi from Roc!" encoded as UTF-8 -## Tcp.write_utf8!(stream, "Hi from Roc!")? -## ``` -## -## > To write unformatted bytes, you can use [Tcp.write!] instead. -write_utf8! : Stream, Str => Result {} [TcpWriteErr StreamErr] -write_utf8! = |stream, str| - write!(stream, Str.to_utf8(str)) - -## Convert a [ConnectErr] to a [Str] you can print. -## -## ``` -## when err is -## TcpPerfomErr(TcpConnectErr(connect_err)) -> -## Stderr.line!(Tcp.connect_err_to_str(connect_err)) -## ``` -## -connect_err_to_str : (ConnectErr _) -> Str -connect_err_to_str = |err| - when err is - PermissionDenied -> "PermissionDenied" - AddrInUse -> "AddrInUse" - AddrNotAvailable -> "AddrNotAvailable" - ConnectionRefused -> "ConnectionRefused" - Interrupted -> "Interrupted" - TimedOut -> "TimedOut" - Unsupported -> "Unsupported" - Unrecognized(message) -> "Unrecognized Error: ${message}" - -## Convert a [StreamErr] to a [Str] you can print. -## -## ``` -## when err is -## TcpPerformErr(TcpReadErr(err)) -> -## err_str = Tcp.stream_err_to_str(err) -## Stderr.line!("Error while reading: ${err_str}") -## -## TcpPerformErr(TcpWriteErr(err)) -> -## err_str = Tcp.stream_err_to_str(err) -## Stderr.line!("Error while writing: ${err_str}") -## ``` -## -stream_err_to_str : StreamErr -> Str -stream_err_to_str = |err| - when err is - StreamNotFound -> "StreamNotFound" - PermissionDenied -> "PermissionDenied" - ConnectionRefused -> "ConnectionRefused" - ConnectionReset -> "ConnectionReset" - Interrupted -> "Interrupted" - OutOfMemory -> "OutOfMemory" - BrokenPipe -> "BrokenPipe" - Unrecognized(message) -> "Unrecognized Error: ${message}" + match err { + "StreamNotFound" => StreamNotFound + "ErrorKind::PermissionDenied" => PermissionDenied + "ErrorKind::ConnectionRefused" => ConnectionRefused + "ErrorKind::ConnectionReset" => ConnectionReset + "ErrorKind::Interrupted" => Interrupted + "ErrorKind::OutOfMemory" => OutOfMemory + "ErrorKind::BrokenPipe" => BrokenPipe + other => Unrecognized(other) + } diff --git a/platform/Tty.roc b/platform/Tty.roc index 8130730c..58d07b75 100644 --- a/platform/Tty.roc +++ b/platform/Tty.roc @@ -1,26 +1,14 @@ ## Provides functionality to change the behaviour of the terminal. ## This is useful for running an app like vim or a game in the terminal. -## -module [ - disable_raw_mode!, - enable_raw_mode!, -] +Tty := [].{ + ## Enable terminal [raw mode](https://en.wikipedia.org/wiki/Terminal_mode) to disable some default terminal bevahiour. + ## + ## This leads to the following changes: + ## - Input will not be echoed to the terminal screen. + ## - Input will be sent straight to the program instead of being buffered (= collected) until the Enter key is pressed. + ## - Special keys like Backspace and CTRL+C will not be processed by the terminal driver but will be passed to the program. + enable_raw_mode! : () => {} -import Host - -## Enable terminal [raw mode](https://en.wikipedia.org/wiki/Terminal_mode) to disable some default terminal bevahiour. -## -## This leads to the following changes: -## - Input will not be echoed to the terminal screen. -## - Input will be sent straight to the program instead of being buffered (= collected) until the Enter key is pressed. -## - Special keys like Backspace and CTRL+C will not be processed by the terminal driver but will be passed to the program. -## -enable_raw_mode! : {} => {} -enable_raw_mode! = |{}| - Host.tty_mode_raw!({}) - -## Revert terminal to default behaviour -## -disable_raw_mode! : {} => {} -disable_raw_mode! = |{}| - Host.tty_mode_canonical!({}) + ## Revert terminal to default behaviour + disable_raw_mode! : () => {} +} diff --git a/platform/Url.roc b/platform/Url.roc deleted file mode 100644 index 5af88029..00000000 --- a/platform/Url.roc +++ /dev/null @@ -1,523 +0,0 @@ -module [ - Url, - append, - from_str, - to_str, - append_param, - has_query, - has_fragment, - query, - fragment, - reserve, - with_query, - with_fragment, - query_params, - path, -] - -## A [Uniform Resource Locator](https://en.wikipedia.org/wiki/URL). -## -## It could be an absolute address, such as `https://roc-lang.org/authors` or -## a relative address, such as `/authors`. You can create one using [Url.from_str]. -Url := Str implements [Inspect] - -## Reserve the given number of bytes as extra capacity. This can avoid reallocation -## when calling multiple functions that increase the length of the URL. -## -## The following example reserves 50 bytes, then builds the url `https://example.com/stuff?caf%C3%A9=du%20Monde&email=hi%40example.com`; -## ``` -## Url.from_str("https://example.com") -## |> Url.reserve(50) -## |> Url.append("stuff") -## |> Url.append_param("café", "du Monde") -## |> Url.append_param("email", "hi@example.com") -## ``` -## The [Str.count_utf8_bytes](https://www.roc-lang.org/builtins/Str#count_utf8_bytes) function can be helpful in finding out how many bytes to reserve. -## -## There is no `Url.with_capacity` because it's better to reserve extra capacity -## on a [Str] first, and then pass that string to [Url.from_str]. This function will make use -## of the extra capacity. -reserve : Url, U64 -> Url -reserve = |@Url(str), cap| - @Url(Str.reserve(str, Num.int_cast(cap))) - -## Create a [Url] without validating or [percent-encoding](https://en.wikipedia.org/wiki/Percent-encoding) -## anything. -## -## ``` -## Url.from_str("https://example.com#stuff") -## ``` -## -## URLs can be absolute, like `https://example.com`, or they can be relative, like `/blah`. -## -## ``` -## Url.from_str("/this/is#relative") -## ``` -## -## Since nothing is validated, this can return invalid URLs. -## -## ``` -## Url.from_str("https://this is not a valid URL, not at all!") -## ``` -## -## Naturally, passing invalid URLs to functions that need valid ones will tend to result in errors. -## -from_str : Str -> Url -from_str = |str| @Url(str) - -## Return a [Str] representation of this URL. -## ``` -## # Gives "https://example.com/two%20words" -## Url.from_str("https://example.com") -## |> Url.append("two words") -## |> Url.to_str -## ``` -to_str : Url -> Str -to_str = |@Url(str)| str - -## [Percent-encodes](https://en.wikipedia.org/wiki/Percent-encoding) a -## [path component](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax) -## and appends to the end of the URL's path. -## -## This will be appended before any queries and fragments. If the given path string begins with `/` and the URL already ends with `/`, one -## will be ignored. This avoids turning a single slash into a double slash. If either the given URL or the given string is empty, no `/` will be added. -## -## ``` -## # Gives https://example.com/some%20stuff -## Url.from_str("https://example.com") -## |> Url.append("some stuff") -## -## # Gives https://example.com/stuff?search=blah#fragment -## Url.from_str("https://example.com?search=blah#fragment") -## |> Url.append("stuff") -## -## # Gives https://example.com/things/stuff/more/etc/" -## Url.from_str("https://example.com/things/") -## |> Url.append("/stuff/") -## |> Url.append("/more/etc/") -## -## # Gives https://example.com/things -## Url.from_str("https://example.com/things") -## |> Url.append("") -## ``` -append : Url, Str -> Url -append = |@Url(url_str), suffix_unencoded| - # percent-encode the suffix but not the slashes - suffix = - suffix_unencoded - |> Str.split_on("/") - |> List.map(percent_encode) - |> Str.join_with("/") - - when Str.split_first(url_str, "?") is - Ok({ before, after }) -> - bytes = - Str.count_utf8_bytes(before) - + 1 # for "/" - + Str.count_utf8_bytes(suffix) - + 1 # for "?" - + Str.count_utf8_bytes(after) - - before - |> Str.reserve(bytes) - |> append_help(suffix) - |> Str.concat("?") - |> Str.concat(after) - |> @Url - - Err(NotFound) -> - # There wasn't a query, but there might still be a fragment - when Str.split_first(url_str, "#") is - Ok({ before, after }) -> - bytes = - Str.count_utf8_bytes(before) - + 1 # for "/" - + Str.count_utf8_bytes(suffix) - + 1 # for "#" - + Str.count_utf8_bytes(after) - - before - |> Str.reserve(bytes) - |> append_help(suffix) - |> Str.concat("#") - |> Str.concat(after) - |> @Url - - Err(NotFound) -> - # No query and no fragment, so just append it - @Url(append_help(url_str, suffix)) - -## Internal helper -append_help : Str, Str -> Str -append_help = |prefix, suffix| - if Str.ends_with(prefix, "/") then - if Str.starts_with(suffix, "/") then - # Avoid a double-slash by appending only the part of the suffix after the "/" - when Str.split_first(suffix, "/") is - Ok({ after }) -> - # TODO `expect before == ""` - Str.concat(prefix, after) - - Err(NotFound) -> - # This should never happen, because we already verified - # that the suffix starts_with "/" - # TODO `expect Bool.false` here with a comment - Str.concat(prefix, suffix) - else - # prefix ends with "/" but suffix doesn't start with one, so just append. - Str.concat(prefix, suffix) - else if Str.starts_with(suffix, "/") then - # Suffix starts with "/" but prefix doesn't end with one, so just append them. - Str.concat(prefix, suffix) - else if Str.is_empty(prefix) then - # Prefix is empty; return suffix. - suffix - else if Str.is_empty(suffix) then - # Suffix is empty; return prefix. - prefix - else - # Neither is empty, but neither has a "/", so add one in between. - prefix - |> Str.concat("/") - |> Str.concat(suffix) - -## Internal helper. This is intentionally unexposed so that you don't accidentally -## double-encode things. If you really want to percent-encode an arbitrary string, -## you can always do: -## -## ``` -## Url.from_str("") -## |> Url.append(my_str_to_encode) -## |> Url.to_str -## ``` -## -## > It is recommended to encode spaces as `%20`, the HTML 2.0 specification -## suggests that these can be encoded as `+`, however this is not always safe to -## use. See [this stackoverflow discussion](https://stackoverflow.com/questions/2678551/when-should-space-be-encoded-to-plus-or-20/47188851#47188851) -## for a detailed explanation. -percent_encode : Str -> Str -percent_encode = |input| - # Optimistically assume we won't need any percent encoding, and can have - # the same capacity as the input string. If we're wrong, it will get doubled. - initial_output = List.with_capacity((Str.count_utf8_bytes(input) |> Num.int_cast)) - - answer = - List.walk( - Str.to_utf8(input), - initial_output, - |output, byte| - # Spec for percent-encoding: https://www.ietf.org/rfc/rfc3986.txt - if - (byte >= 97 and byte <= 122) # lowercase ASCII - or (byte >= 65 and byte <= 90) # uppercase ASCII - or (byte >= 48 and byte <= 57) # digit - then - # This is the most common case: an unreserved character, - # which needs no encoding in a path - List.append(output, byte) - else - when byte is - 46 # '.' - | 95 # '_' - | 126 # '~' - | 150 -> # '-' - # These special characters can all be unescaped in paths - List.append(output, byte) - - _ -> - # This needs encoding in a path - suffix = - Str.to_utf8(percent_encoded) - |> List.sublist({ len: 3, start: 3 * Num.int_cast(byte) }) - - List.concat(output, suffix), - ) - - Str.from_utf8(answer) - |> Result.with_default("") # This should never fail - -## Adds a [Str] query parameter to the end of the [Url]. -## -## The key and value both get [percent-encoded](https://en.wikipedia.org/wiki/Percent-encoding). -## -## ``` -## # Gives https://example.com?email=someone%40example.com -## Url.from_str("https://example.com") -## |> Url.append_param("email", "someone@example.com") -## ``` -## -## This can be called multiple times on the same URL. -## -## ``` -## # Gives https://example.com?caf%C3%A9=du%20Monde&email=hi%40example.com -## Url.from_str("https://example.com") -## |> Url.append_param("café", "du Monde") -## |> Url.append_param("email", "hi@example.com") -## ``` -## -append_param : Url, Str, Str -> Url -append_param = |@Url(url_str), key, value| - { without_fragment, after_query } = - when Str.split_last(url_str, "#") is - Ok({ before, after }) -> - # The fragment is almost certainly going to be a small string, - # so this interpolation should happen on the stack. - { without_fragment: before, after_query: "#${after}" } - - Err(NotFound) -> - { without_fragment: url_str, after_query: "" } - - encoded_key = percent_encode(key) - encoded_value = percent_encode(value) - - bytes = - Str.count_utf8_bytes(without_fragment) - + 1 # for "?" or "&" - + Str.count_utf8_bytes(encoded_key) - + 1 # for "=" - + Str.count_utf8_bytes(encoded_value) - + Str.count_utf8_bytes(after_query) - - without_fragment - |> Str.reserve(bytes) - |> Str.concat((if has_query(@Url(without_fragment)) then "&" else "?")) - |> Str.concat(encoded_key) - |> Str.concat("=") - |> Str.concat(encoded_value) - |> Str.concat(after_query) - |> @Url - -## Replaces the URL's [query](https://en.wikipedia.org/wiki/URL#Syntax)—the part -## after the `?`, if it has one, but before any `#` it might have. -## -## Passing `""` removes the `?` (if there was one). -## -## ``` -## # Gives https://example.com?newQuery=thisRightHere#stuff -## Url.from_str("https://example.com?key1=val1&key2=val2#stuff") -## |> Url.with_query("newQuery=thisRightHere") -## -## # Gives https://example.com#stuff -## Url.from_str("https://example.com?key1=val1&key2=val2#stuff") -## |> Url.with_query("") -## ``` -with_query : Url, Str -> Url -with_query = |@Url(url_str), query_str| - { without_fragment, after_query } = - when Str.split_last(url_str, "#") is - Ok({ before, after }) -> - # The fragment is almost certainly going to be a small string, - # so this interpolation should happen on the stack. - { without_fragment: before, after_query: "#${after}" } - - Err(NotFound) -> - { without_fragment: url_str, after_query: "" } - - before_query = - when Str.split_last(without_fragment, "?") is - Ok({ before }) -> before - Err(NotFound) -> without_fragment - - if Str.is_empty(query_str) then - @Url(Str.concat(before_query, after_query)) - else - bytes = - Str.count_utf8_bytes(before_query) - + 1 # for "?" - + Str.count_utf8_bytes(query_str) - + Str.count_utf8_bytes(after_query) - - before_query - |> Str.reserve(bytes) - |> Str.concat("?") - |> Str.concat(query_str) - |> Str.concat(after_query) - |> @Url - -## Returns the URL's [query](https://en.wikipedia.org/wiki/URL#Syntax)—the part after -## the `?`, if it has one, but before any `#` it might have. -## -## Returns `""` if the URL has no query. -## -## ``` -## # Gives "key1=val1&key2=val2&key3=val3" -## Url.from_str("https://example.com?key1=val1&key2=val2&key3=val3#stuff") -## |> Url.query -## -## # Gives "" -## Url.from_str("https://example.com#stuff") -## |> Url.query -## ``` -## -query : Url -> Str -query = |@Url(url_str)| - without_fragment = - when Str.split_last(url_str, "#") is - Ok({ before }) -> before - Err(NotFound) -> url_str - - when Str.split_last(without_fragment, "?") is - Ok({ after }) -> after - Err(NotFound) -> "" - -## Returns [Bool.true] if the URL has a `?` in it. -## -## ``` -## # Gives Bool.true -## Url.from_str("https://example.com?key=value#stuff") -## |> Url.has_query -## -## # Gives Bool.false -## Url.from_str("https://example.com#stuff") -## |> Url.has_query -## ``` -## -has_query : Url -> Bool -has_query = |@Url(url_str)| - Str.contains(url_str, "?") - -## Returns the URL's [fragment](https://en.wikipedia.org/wiki/URL#Syntax)—the part after -## the `#`, if it has one. -## -## Returns `""` if the URL has no fragment. -## -## ``` -## # Gives "stuff" -## Url.from_str("https://example.com#stuff") -## |> Url.fragment -## -## # Gives "" -## Url.from_str("https://example.com") -## |> Url.fragment -## ``` -## -fragment : Url -> Str -fragment = |@Url(url_str)| - when Str.split_last(url_str, "#") is - Ok({ after }) -> after - Err(NotFound) -> "" - -## Replaces the URL's [fragment](https://en.wikipedia.org/wiki/URL#Syntax). -## -## If the URL didn't have a fragment, adds one. Passing `""` removes the fragment. -## -## ``` -## # Gives https://example.com#things -## Url.from_str("https://example.com#stuff") -## |> Url.with_fragment("things") -## -## # Gives https://example.com#things -## Url.from_str("https://example.com") -## |> Url.with_fragment("things") -## -## # Gives https://example.com -## Url.from_str("https://example.com#stuff") -## |> Url.with_fragment "" -## ``` -## -with_fragment : Url, Str -> Url -with_fragment = |@Url(url_str), fragment_str| - when Str.split_last(url_str, "#") is - Ok({ before }) -> - if Str.is_empty(fragment_str) then - # If the given fragment is empty, remove the URL's fragment - @Url(before) - else - # Replace the URL's old fragment with this one, discarding `after` - @Url("${before}#${fragment_str}") - - Err(NotFound) -> - if Str.is_empty(fragment_str) then - # If the given fragment is empty, leave the URL as having no fragment - @Url(url_str) - else - # The URL didn't have a fragment, so give it this one - @Url("${url_str}#${fragment_str}") - -## Returns [Bool.true] if the URL has a `#` in it. -## -## ``` -## # Gives Bool.true -## Url.from_str("https://example.com?key=value#stuff") -## |> Url.has_fragment -## -## # Gives Bool.false -## Url.from_str("https://example.com?key=value") -## |> Url.has_fragment -## ``` -## -has_fragment : Url -> Bool -has_fragment = |@Url(url_str)| - Str.contains(url_str, "#") - -# Adapted from the percent-encoding crate, © The rust-url developers, Apache2-licensed -# -# https://github.com/servo/rust-url/blob/e12d76a61add5bc09980599c738099feaacd1d0d/percent_encoding/src/lib.rs#L183 -percent_encoded : Str -percent_encoded = "%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF" - -query_params : Url -> Dict Str Str -query_params = |url| - query(url) - |> Str.split_on("&") - |> List.walk( - Dict.empty({}), - |dict, pair| - when Str.split_first(pair, "=") is - Ok({ before, after }) -> Dict.insert(dict, before, after) - Err(NotFound) -> Dict.insert(dict, pair, ""), - ) - -## Returns the URL's [path](https://en.wikipedia.org/wiki/URL#Syntax)—the part after -## the scheme and authority (e.g. `https://`) but before any `?` or `#` it might have. -## -## Returns `""` if the URL has no path. -## -## ``` -## # Gives "example.com/" -## Url.from_str("https://example.com/?key1=val1&key2=val2&key3=val3#stuff") -## |> Url.path -## ``` -## -## ``` -## # Gives "/foo/" -## Url.from_str("/foo/?key1=val1&key2=val2&key3=val3#stuff") -## |> Url.path -## ``` -path : Url -> Str -path = |@Url(url_str)| - without_authority = - if Str.starts_with(url_str, "/") then - url_str - else - when Str.split_first(url_str, ":") is - Ok({ after }) -> - when Str.split_first(after, "//") is - # Only drop the `//` if it's right after the `://` like in `https://` - # (so, `before` is empty) - otherwise, the `//` is part of the path! - Ok({ before, after: after_slashes }) if Str.is_empty(before) -> after_slashes - _ -> after - - # There's no `//` and also no `:` so this must be a path-only URL, e.g. "/foo?bar=baz#blah" - Err(NotFound) -> url_str - - # Drop the query and/or fragment - when Str.split_last(without_authority, "?") is - Ok({ before }) -> before - Err(NotFound) -> - when Str.split_last(without_authority, "#") is - Ok({ before }) -> before - Err(NotFound) -> without_authority - -# `Url.path` supports non-encoded URIs in query parameters (https://datatracker.ietf.org/doc/html/rfc3986#section-3.4) -expect - input = Url.from_str("https://example.com/foo/bar?key1=https://www.baz.com/some-path#stuff") - expected = "example.com/foo/bar" - path(input) == expected - -# `Url.path` supports non-encoded URIs in query parameters (https://datatracker.ietf.org/doc/html/rfc3986#section-3.4) -expect - input = Url.from_str("/foo/bar?key1=https://www.baz.com/some-path#stuff") - output = Url.path(input) - expected = "/foo/bar" - output == expected diff --git a/platform/Utc.roc b/platform/Utc.roc index 68ba767b..3483d70f 100644 --- a/platform/Utc.roc +++ b/platform/Utc.roc @@ -1,79 +1,23 @@ -module [ - Utc, - now!, - to_millis_since_epoch, - from_millis_since_epoch, - to_nanos_since_epoch, - from_nanos_since_epoch, - delta_as_millis, - delta_as_nanos, - to_iso_8601, -] - -import Host -import InternalDateTime - -## Stores a timestamp as nanoseconds since UNIX EPOCH -Utc := I128 implements [Inspect] - -## Duration since UNIX EPOCH -now! : {} => Utc -now! = |{}| - @Utc(Num.to_i128(Host.posix_time!({}))) - -# Constant number of nanoseconds in a millisecond -nanos_per_milli = 1_000_000 - -## Convert Utc timestamp to milliseconds -to_millis_since_epoch : Utc -> I128 -to_millis_since_epoch = |@Utc(nanos)| - nanos // nanos_per_milli - -## Convert milliseconds to Utc timestamp -from_millis_since_epoch : I128 -> Utc -from_millis_since_epoch = |millis| - @Utc((millis * nanos_per_milli)) - -## Convert Utc timestamp to nanoseconds -to_nanos_since_epoch : Utc -> I128 -to_nanos_since_epoch = |@Utc(nanos)| - nanos - -## Convert nanoseconds to Utc timestamp -from_nanos_since_epoch : I128 -> Utc -from_nanos_since_epoch = @Utc - -## Calculate milliseconds between two Utc timestamps -delta_as_millis : Utc, Utc -> U128 -delta_as_millis = |utc_a, utc_b| - (delta_as_nanos(utc_a, utc_b)) // nanos_per_milli - -## Calculate nanoseconds between two Utc timestamps -delta_as_nanos : Utc, Utc -> U128 -delta_as_nanos = |@Utc(nanos_a), @Utc(nanos_b)| - # bitwise_xor for best performance - nanos_a_shifted = Num.bitwise_xor(Num.to_u128(nanos_a), Num.shift_left_by(1, 127)) - nanos_b_shifted = Num.bitwise_xor(Num.to_u128(nanos_b), Num.shift_left_by(1, 127)) - - Num.abs_diff(nanos_a_shifted, nanos_b_shifted) - -## Convert Utc timestamp to ISO 8601 string. -## For example: 2023-11-14T23:39:39Z -to_iso_8601 : Utc -> Str -to_iso_8601 = |@Utc(nanos)| - nanos - |> Num.div_trunc(nanos_per_milli) - |> InternalDateTime.epoch_millis_to_datetime - |> InternalDateTime.to_iso_8601 - -# TESTS -expect delta_as_nanos(from_nanos_since_epoch(0), from_nanos_since_epoch(0)) == 0 -expect delta_as_nanos(from_nanos_since_epoch(1), from_nanos_since_epoch(2)) == 1 -expect delta_as_nanos(from_nanos_since_epoch(-1), from_nanos_since_epoch(1)) == 2 -expect delta_as_nanos(from_nanos_since_epoch(Num.min_i128), from_nanos_since_epoch(Num.max_i128)) == Num.max_u128 - -expect delta_as_millis(from_millis_since_epoch(0), from_millis_since_epoch(0)) == 0 -expect delta_as_millis(from_nanos_since_epoch(1), from_nanos_since_epoch(2)) == 0 -expect delta_as_millis(from_millis_since_epoch(1), from_millis_since_epoch(2)) == 1 -expect delta_as_millis(from_millis_since_epoch(-1), from_millis_since_epoch(1)) == 2 -expect delta_as_millis(from_nanos_since_epoch(Num.min_i128), from_nanos_since_epoch(Num.max_i128)) == Num.max_u128 // nanos_per_milli +Utc := [].{ + ## 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/glue-internal-arg.roc b/platform/glue-internal-arg.roc deleted file mode 100644 index b4ff8a02..00000000 --- a/platform/glue-internal-arg.roc +++ /dev/null @@ -1,20 +0,0 @@ -# This file isn't used per-se, I have left it here to help with generating rust glue for the platform -# In future glue types may be all generated from the platform file, but for now these are semi-automated. -# -# You can generate "glue" types using the following, though this feature is a WIP so things will need to -# be manually adjusted after generation. -# -# ``` -# $ roc glue ../roc/crates/glue/src/RustGlue.roc asdf/ platform/glue-internal-arg.roc -# ``` -platform "glue-types" - requires {} { main : _ } - exposes [] - packages {} - imports [] - provides [main_for_host] - -import InternalArg - -main_for_host : InternalArg.ArgToAndFromHost -main_for_host = main diff --git a/platform/glue-internal-cmd.roc b/platform/glue-internal-cmd.roc deleted file mode 100644 index 4a113622..00000000 --- a/platform/glue-internal-cmd.roc +++ /dev/null @@ -1,20 +0,0 @@ -# This file isn't used per-se, I have left it here to help with generating rust glue for the platform -# In future glue types may be all generated from the platform file, but for now these are semi-automated. -# -# You can generate "glue" types using the following, though this feature is a WIP so things will need to -# be manually adjusted after generation. -# -# ``` -# $ roc glue ../roc/crates/glue/src/RustGlue.roc asdf/ platform/glue-internal-cmd.roc -# ``` -platform "glue-types" - requires {} { main : _ } - exposes [] - packages {} - imports [] - provides [main_for_host] - -import InternalCmd - -main_for_host : InternalCmd.OutputFromHost -main_for_host = main diff --git a/platform/glue-internal-http.roc b/platform/glue-internal-http.roc deleted file mode 100644 index 6505bbef..00000000 --- a/platform/glue-internal-http.roc +++ /dev/null @@ -1,23 +0,0 @@ -# This file isn't used per-se, I have left it here to help with generating rust glue for the platform -# In future glue types may be all generated from the platform file, but for now these are semi-automated. -# -# You can generate "glue" types using the following, though this feature is a WIP so things will need to -# be manually adjusted after generation. -# -# ``` -# $ roc glue ../roc/crates/glue/src/RustGlue.roc asdf/ platform/glue-internal-http.roc -# ``` -platform "glue-types" - requires {} { main : _ } - exposes [] - packages {} - imports [] - provides [main_for_host] - -import InternalHttp - -main_for_host : { - a : InternalHttp.RequestToAndFromHost, - b : InternalHttp.ResponseToAndFromHost, -} -main_for_host = main diff --git a/platform/libapp.roc b/platform/libapp.roc deleted file mode 100644 index 41c2a21e..00000000 --- a/platform/libapp.roc +++ /dev/null @@ -1,7 +0,0 @@ -app [main!] { pf: platform "main.roc" } - -# Throw an error here so we can easily confirm the host -# executable built correctly just by running it. -main! : _ => Result {} [Exit I32 Str]_ -main! = |_args| - Err(JustAStub) diff --git a/platform/main.roc b/platform/main.roc index 08d8adeb..4c20760d 100644 --- a/platform/main.roc +++ b/platform/main.roc @@ -1,69 +1,102 @@ -platform "cli" - requires {} { main! : List Arg.Arg => Result {} [Exit I32 Str]_ } - exposes [ - Path, - Arg, - Dir, - Env, - File, - Http, - Stderr, - Stdin, - Stdout, - Tcp, - Url, - Utc, - Sleep, - Cmd, - Tty, - Locale, - Sqlite, - Random, - ] +platform "" + requires {} { main! : List(Str) => Try({}, [Exit(I32), ..]) } + exposes [Cmd, Dir, Env, File, Http, IOErr, InternalHttp, InternalSqlite, Locale, Path, Random, Sleep, Sqlite, Stdin, Stdout, Stderr, Tcp, Tty, Utc] packages {} - imports [] - provides [main_for_host!] + provides { "roc_main": main_for_host! } + hosted { + "hosted_cmd_host_exec_exit_code": Cmd.host_exec_exit_code!, + "hosted_cmd_host_exec_output": Cmd.host_exec_output!, + "hosted_dir_create": Dir.create!, + "hosted_dir_create_all": Dir.create_all!, + "hosted_dir_delete_all": Dir.delete_all!, + "hosted_dir_delete_empty": Dir.delete_empty!, + "hosted_dir_list": Dir.list!, + "hosted_env_cwd": Env.cwd!, + "hosted_env_exe_path": Env.exe_path!, + "hosted_env_temp_dir": Env.temp_dir!, + "hosted_env_var": Env.var!, + "hosted_file_delete": File.delete!, + "hosted_file_is_executable": File.is_executable!, + "hosted_file_is_readable": File.is_readable!, + "hosted_file_is_writable": File.is_writable!, + "hosted_file_read_bytes": File.read_bytes!, + "hosted_file_read_utf8": File.read_utf8!, + "hosted_file_size_in_bytes": File.size_in_bytes!, + "hosted_file_time_accessed": File.time_accessed!, + "hosted_file_time_created": File.time_created!, + "hosted_file_time_modified": File.time_modified!, + "hosted_file_write_bytes": File.write_bytes!, + "hosted_file_write_utf8": File.write_utf8!, + "hosted_locale_all": Locale.all!, + "hosted_locale_get": Locale.get!, + "hosted_path_type": Path.host_path_type!, + "hosted_random_seed_u32": Random.seed_u32!, + "hosted_random_seed_u64": Random.seed_u64!, + "hosted_sleep_millis": Sleep.millis!, + "hosted_stderr_line": Stderr.line!, + "hosted_stderr_write": Stderr.write!, + "hosted_stderr_write_bytes": Stderr.write_bytes!, + "hosted_stdin_bytes": Stdin.bytes!, + "hosted_stdin_line": Stdin.line!, + "hosted_stdin_read_to_end": Stdin.read_to_end!, + "hosted_stdout_line": Stdout.line!, + "hosted_stdout_write": Stdout.write!, + "hosted_stdout_write_bytes": Stdout.write_bytes!, + "hosted_tty_disable_raw_mode": Tty.disable_raw_mode!, + "hosted_tty_enable_raw_mode": Tty.enable_raw_mode!, + "hosted_utc_now": Utc.now!, + # 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": Sqlite.host_prepare!, + "hosted_sqlite_bind": Sqlite.host_bind!, + "hosted_sqlite_columns": Sqlite.host_columns!, + "hosted_sqlite_column_value": Sqlite.host_column_value!, + "hosted_sqlite_step": Sqlite.host_step!, + "hosted_sqlite_reset": Sqlite.host_reset!, + # TCP hosted functions are likewise kept at the end to avoid renumbering. + "hosted_tcp_connect": Tcp.host_connect!, + "hosted_tcp_read_up_to": Tcp.host_read_up_to!, + "hosted_tcp_read_exactly": Tcp.host_read_exactly!, + "hosted_tcp_read_until": Tcp.host_read_until!, + "hosted_tcp_write": Tcp.host_write!, + # HTTP is likewise kept at the end to avoid renumbering glue types. + "hosted_http_send_request": Http.host_send_request!, + } + targets: { + inputs_dir: "targets/", + x64mac: { inputs: ["libhost.a", app] }, + arm64mac: { inputs: ["libhost.a", app] }, + x64musl: { inputs: ["crt1.o", "libhost.a", "libunwind.a", app, "libc.a"] }, + arm64musl: { inputs: ["crt1.o", "libhost.a", "libunwind.a", app, "libc.a"] }, + } -import Arg +import Cmd +import Dir +import Env +import File +import Http +import IOErr +import InternalHttp +import InternalSqlite +import Locale +import Path +import Random +import Sleep +import Sqlite +import Stdin +import Stdout import Stderr -import InternalArg +import Tcp +import Tty +import Utc -main_for_host! : List InternalArg.ArgToAndFromHost => I32 -main_for_host! = |raw_args| - - args = - raw_args - |> List.map(InternalArg.to_os_raw) - |> List.map(Arg.from_os_raw) - - when main!(args) is - Ok({}) -> 0 - Err(Exit(code, msg)) -> - if Str.is_empty(msg) then - code - else - _ = Stderr.line!(msg) - code - - Err(err) -> - err_str = Inspect.to_str(err) - - clean_err_str = - # Inspect adds parentheses around errors, which are unnecessary here. - if Str.starts_with(err_str, "(") and Str.ends_with(err_str, ")") then - err_str - |> Str.replace_first("(", "") - |> Str.replace_last(")", "") - else - err_str - - help_msg = - """ - - Program exited with error: - - ❌ ${clean_err_str} - """ - - _ = Stderr.line!(help_msg) - 1 +main_for_host! : List(Str) => I32 +main_for_host! = |args| + match main!(args) { + Ok({}) => 0 + Err(Exit(code)) => code + Err(other) => + match Stderr.line!("Program exited with error: ${Str.inspect(other)}") { + _ => 1 + } + } diff --git a/platform/targets/arm64musl/crt1.o b/platform/targets/arm64musl/crt1.o new file mode 100755 index 00000000..508b0767 Binary files /dev/null and b/platform/targets/arm64musl/crt1.o differ diff --git a/platform/targets/arm64musl/libc.a b/platform/targets/arm64musl/libc.a new file mode 100755 index 00000000..1607a18a Binary files /dev/null and b/platform/targets/arm64musl/libc.a differ diff --git a/platform/targets/arm64musl/libunwind.a b/platform/targets/arm64musl/libunwind.a new file mode 100644 index 00000000..b7bd77c5 Binary files /dev/null and b/platform/targets/arm64musl/libunwind.a differ diff --git a/platform/targets/x64musl/crt1.o b/platform/targets/x64musl/crt1.o new file mode 100644 index 00000000..ea17e8a5 Binary files /dev/null and b/platform/targets/x64musl/crt1.o differ diff --git a/platform/targets/x64musl/libc.a b/platform/targets/x64musl/libc.a new file mode 100644 index 00000000..49b6986c Binary files /dev/null and b/platform/targets/x64musl/libc.a differ diff --git a/platform/targets/x64musl/libunwind.a b/platform/targets/x64musl/libunwind.a new file mode 100644 index 00000000..636d5210 Binary files /dev/null and b/platform/targets/x64musl/libunwind.a differ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..fa8117a4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2434 @@ +//! Roc platform host implementation for Roc's direct-symbol host ABI. + +#![allow(improper_ctypes_definitions)] + +use core::mem::ManuallyDrop; +use std::cell::RefCell; +use std::ffi::{c_char, c_int, c_void, CStr, CString}; +use std::fs; +use std::io::{self, BufRead, BufReader, Read, Write}; +use std::net::TcpStream; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; + +mod roc_platform_abi; + +use crate::roc_platform_abi::*; + +// RustGlue assigns numbered names (TryTypeN, IOErrTypeN, ...) to anonymous Roc +// records and result types, and the numbers shift whenever a module is added. +// To stay robust against that renumbering we alias against the *semantic* names +// the generator also emits (e.g. `CmdHostExecExitCodeResult`), which are keyed by +// module + function name and therefore stable. Where our preferred local name is +// identical to a generated semantic alias (e.g. `CmdIOErr`, `DirListResult`), we +// omit it here and rely on the `use crate::roc_platform_abi::*;` glob above. +type CmdExitResult = CmdHostExecExitCodeResult; +type CmdExitResultPayload = CmdHostExecExitCodeResultPayload; +type CmdExitResultTag = CmdHostExecExitCodeResultTag; +type CmdOutputResult = CmdHostExecOutputResult; +type CmdOutputResultPayload = CmdHostExecOutputResultPayload; +type CmdOutputResultTag = CmdHostExecOutputResultTag; +type CmdOutputFailureResult = CmdHostExecOutputErrResult; +type CmdOutputFailureResultPayload = CmdHostExecOutputErrResultPayload; +type CmdOutputFailureResultTag = CmdHostExecOutputErrResultTag; +type CmdOutputFailure = CmdHostExecOutputErrOk; +type CmdOutputSuccess = CmdHostExecOutputOk; + +type DirUnitResult = DirCreateResult; +type DirUnitResultPayload = DirCreateResultPayload; +type DirUnitResultTag = DirCreateResultTag; + +type FileBytesResult = FileReadBytesResult; +type FileBytesResultPayload = FileReadBytesResultPayload; +type FileBytesResultTag = FileReadBytesResultTag; +type FileStrResult = FileReadUtf8Result; +type FileStrResultPayload = FileReadUtf8ResultPayload; +type FileStrResultTag = FileReadUtf8ResultTag; +type FileSizeResult = FileSizeInBytesResult; +type FileSizeResultPayload = FileSizeInBytesResultPayload; +type FileSizeResultTag = FileSizeInBytesResultTag; +type FileBoolResult = FileIsExecutableResult; +type FileBoolResultPayload = FileIsExecutableResultPayload; +type FileBoolResultTag = FileIsExecutableResultTag; +type FileTimeResult = FileTimeAccessedResult; +type FileTimeResultPayload = FileTimeAccessedResultPayload; +type FileTimeResultTag = FileTimeAccessedResultTag; + +type PathTypeResult = PathHostPathTypeResult; +type PathTypeResultPayload = PathHostPathTypeResultPayload; +type PathTypeResultTag = PathHostPathTypeResultTag; +type PathInfo = PathHostPathTypeOk; + +type RandomU64Result = RandomSeedU64Result; +type RandomU64ResultPayload = RandomSeedU64ResultPayload; +type RandomU64ResultTag = RandomSeedU64ResultTag; +type RandomU32Result = RandomSeedU32Result; +type RandomU32ResultPayload = RandomSeedU32ResultPayload; +type RandomU32ResultTag = RandomSeedU32ResultTag; + +type StderrUnitResult = StderrLineResult; +type StderrUnitResultPayload = StderrLineResultPayload; +type StderrUnitResultTag = StderrLineResultTag; +type StderrBytesResult = StderrWriteBytesResult; +type StderrBytesResultPayload = StderrWriteBytesResultPayload; +type StderrBytesResultTag = StderrWriteBytesResultTag; + +// The stdin read-error tag unions have no semantic alias, so reference the +// numbered glue types directly (update these if the glue renumbers them). +type StdinLineReadErr = EndOfFileOrStdinErrType112; +type StdinLineReadErrPayload = EndOfFileOrStdinErrType112Payload; +type StdinLineReadErrTag = EndOfFileOrStdinErrType112Tag; +type StdinBytesReadErr = EndOfFileOrStdinErrType117; +type StdinBytesReadErrPayload = EndOfFileOrStdinErrType117Payload; +type StdinBytesReadErrTag = EndOfFileOrStdinErrType117Tag; + +type StdoutUnitResult = StdoutLineResult; +type StdoutUnitResultPayload = StdoutLineResultPayload; +type StdoutUnitResultTag = StdoutLineResultTag; +type StdoutBytesResult = StdoutWriteBytesResult; +type StdoutBytesResultPayload = StdoutWriteBytesResultPayload; +type StdoutBytesResultTag = StdoutWriteBytesResultTag; + +// ============================================================================ +// SQLite +// +// The generated glue represents `Sqlite.Stmt` (a `Box(U64)`) as `*mut u64`: a +// boxed u64 whose value we use to stash a raw `*mut SqliteStatement`. The box is +// allocated/refcounted with the generated `allocate_box`/`decref_box_with` +// helpers; teardown (running `sqlite3_finalize`) happens in `drop_sqlite_stmt` +// when the last reference is released. Each host fn that takes a handle calls +// `release_sqlite_stmt` before returning to balance the incref Roc performs when +// the value stays live. +// ---------------------------------------------------------------------------- + +// Generated value/error/state types (see src/roc_platform_abi.rs). +type SqliteValue = BytesOrIntegerOrNullOrRealOrString; +type SqliteValueTag = BytesOrIntegerOrNullOrRealOrStringTag; +type SqliteValuePayload = BytesOrIntegerOrNullOrRealOrStringPayload; +type SqliteError = AnonStruct85; +type SqliteBindings = AnonStruct93; + +const SQLITE_STMT_BOX_ALIGN: usize = core::mem::align_of::(); + +struct SqliteStatement { + connection: *mut libsqlite3_sys::sqlite3, + stmt: *mut libsqlite3_sys::sqlite3_stmt, +} + +impl Drop for SqliteStatement { + fn drop(&mut self) { + unsafe { + libsqlite3_sys::sqlite3_finalize(self.stmt); + } + } +} + +thread_local! { + // Connections are cached per database path and live until process exit. + static SQLITE_CONNECTIONS: RefCell> = + const { RefCell::new(Vec::new()) }; +} + +fn box_sqlite_stmt(stmt: SqliteStatement, roc_host: &RocHost) -> *mut u64 { + let raw: *mut SqliteStatement = Box::into_raw(Box::new(stmt)); + let boxed = allocate_box( + core::mem::size_of::(), + SQLITE_STMT_BOX_ALIGN, + false, + roc_host, + ); + unsafe { + *(boxed as *mut u64) = raw as u64; + } + boxed as *mut u64 +} + +unsafe fn sqlite_stmt_ref<'a>(handle: *mut u64) -> &'a mut SqliteStatement { + &mut *(*handle as *mut SqliteStatement) +} + +extern "C" fn drop_sqlite_stmt(data_ptr: *mut c_void, _roc_host: *mut RocHost) { + unsafe { + let raw = *(data_ptr as *mut u64) as *mut SqliteStatement; + if !raw.is_null() { + drop(Box::from_raw(raw)); + } + } +} + +fn release_sqlite_stmt(handle: *mut u64, roc_host: &RocHost) { + decref_box_with( + handle as RocBox, + SQLITE_STMT_BOX_ALIGN, + // The boxed payload is a raw `u64` (a pointer to our SqliteStatement), + // not a Roc-refcounted value — this must match the `false` passed to + // `allocate_box` in `box_sqlite_stmt`, independent of the teardown callback. + false, + Some(drop_sqlite_stmt), + roc_host, + ); +} + +// SQLITE_TRANSIENT tells SQLite to make its own copy of bound text/blob data, so +// we don't have to keep the Roc-owned bytes alive past the bind call. +fn sqlite_transient() -> Option { + Some(unsafe { + core::mem::transmute::<*const c_void, unsafe extern "C" fn(*mut c_void)>( + -1isize as *const c_void, + ) + }) +} + +fn sqlite_errmsg(connection: *mut libsqlite3_sys::sqlite3, code: c_int) -> String { + unsafe { + let mut message = CStr::from_ptr(libsqlite3_sys::sqlite3_errstr(code)) + .to_string_lossy() + .into_owned(); + if !connection.is_null() { + let detailed = libsqlite3_sys::sqlite3_errmsg(connection); + if !detailed.is_null() { + message = CStr::from_ptr(detailed).to_string_lossy().into_owned(); + } + } + message + } +} + +fn sqlite_error(code: c_int, message: &str, roc_host: &RocHost) -> SqliteError { + SqliteError { + code: code as i64, + message: RocStr::from_str(message, roc_host), + } +} + +fn sqlite_err_from_stmt(stmt: &SqliteStatement, code: c_int, roc_host: &RocHost) -> SqliteError { + let message = sqlite_errmsg(stmt.connection, code); + sqlite_error(code, &message, roc_host) +} + +fn sqlite_get_connection(path: &str) -> Result<*mut libsqlite3_sys::sqlite3, (c_int, String)> { + SQLITE_CONNECTIONS.with(|cell| { + for (conn_path, connection) in cell.borrow().iter() { + if conn_path.as_bytes() == path.as_bytes() { + return Ok(*connection); + } + } + + let cpath = CString::new(path).map_err(|_| { + ( + libsqlite3_sys::SQLITE_ERROR, + "database path contained an interior nul byte".to_string(), + ) + })?; + let mut connection: *mut libsqlite3_sys::sqlite3 = core::ptr::null_mut(); + let flags = libsqlite3_sys::SQLITE_OPEN_CREATE + | libsqlite3_sys::SQLITE_OPEN_READWRITE + | libsqlite3_sys::SQLITE_OPEN_NOMUTEX; + let err = unsafe { + libsqlite3_sys::sqlite3_open_v2( + cpath.as_ptr(), + &mut connection, + flags, + core::ptr::null(), + ) + }; + if err != libsqlite3_sys::SQLITE_OK { + let message = sqlite_errmsg(connection, err); + return Err((err, message)); + } + + cell.borrow_mut().push((cpath, connection)); + Ok(connection) + }) +} + +fn sqlite_value_integer(value: i64) -> SqliteValue { + SqliteValue { + payload: SqliteValuePayload { + integer: ManuallyDrop::new(value), + }, + tag: SqliteValueTag::Integer, + } +} + +fn sqlite_value_real(value: f64) -> SqliteValue { + SqliteValue { + payload: SqliteValuePayload { + real: ManuallyDrop::new(value), + }, + tag: SqliteValueTag::Real, + } +} + +fn sqlite_value_string(value: RocStr) -> SqliteValue { + SqliteValue { + payload: SqliteValuePayload { + string: ManuallyDrop::new(value), + }, + tag: SqliteValueTag::String, + } +} + +fn sqlite_value_bytes(value: RocListWith) -> SqliteValue { + SqliteValue { + payload: SqliteValuePayload { + bytes: ManuallyDrop::new(value), + }, + tag: SqliteValueTag::Bytes, + } +} + +fn sqlite_value_null() -> SqliteValue { + SqliteValue { + payload: SqliteValuePayload { null: [] }, + tag: SqliteValueTag::Null, + } +} + +fn try_sqlite_prepare_ok(handle: *mut u64) -> SqliteHostPrepareResult { + SqliteHostPrepareResult { + payload: SqliteHostPrepareResultPayload { + ok: ManuallyDrop::new(handle), + }, + tag: SqliteHostPrepareResultTag::Ok, + } +} + +fn try_sqlite_prepare_err(error: SqliteError) -> SqliteHostPrepareResult { + SqliteHostPrepareResult { + payload: SqliteHostPrepareResultPayload { + err: ManuallyDrop::new(error), + }, + tag: SqliteHostPrepareResultTag::Err, + } +} + +fn try_sqlite_unit_ok() -> SqliteHostBindResult { + SqliteHostBindResult { + payload: SqliteHostBindResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: SqliteHostBindResultTag::Ok, + } +} + +fn try_sqlite_unit_err(error: SqliteError) -> SqliteHostBindResult { + SqliteHostBindResult { + payload: SqliteHostBindResultPayload { + err: ManuallyDrop::new(error), + }, + tag: SqliteHostBindResultTag::Err, + } +} + +fn try_sqlite_value_ok(value: SqliteValue) -> SqliteHostColumnValueResult { + SqliteHostColumnValueResult { + payload: SqliteHostColumnValueResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: SqliteHostColumnValueResultTag::Ok, + } +} + +fn try_sqlite_value_err(error: SqliteError) -> SqliteHostColumnValueResult { + SqliteHostColumnValueResult { + payload: SqliteHostColumnValueResultPayload { + err: ManuallyDrop::new(error), + }, + tag: SqliteHostColumnValueResultTag::Err, + } +} + +// `host_step!` marshals a Bool: true => a row is ready (SQLITE_ROW), +// false => the statement is done (SQLITE_DONE). +fn try_sqlite_step_ok(has_row: bool) -> SqliteHostStepResult { + SqliteHostStepResult { + payload: SqliteHostStepResultPayload { + ok: ManuallyDrop::new(has_row), + }, + tag: SqliteHostStepResultTag::Ok, + } +} + +fn try_sqlite_step_err(error: SqliteError) -> SqliteHostStepResult { + SqliteHostStepResult { + payload: SqliteHostStepResultPayload { + err: ManuallyDrop::new(error), + }, + tag: SqliteHostStepResultTag::Err, + } +} + +unsafe fn sqlite_bind_one( + stmt: *mut libsqlite3_sys::sqlite3_stmt, + index: c_int, + value: &SqliteValue, +) -> c_int { + match value.tag { + SqliteValueTag::Integer => { + libsqlite3_sys::sqlite3_bind_int64(stmt, index, *value.payload.integer) + } + SqliteValueTag::Real => { + libsqlite3_sys::sqlite3_bind_double(stmt, index, *value.payload.real) + } + SqliteValueTag::String => { + let text = value.payload.string.as_str(); + libsqlite3_sys::sqlite3_bind_text64( + stmt, + index, + text.as_ptr() as *const c_char, + text.len() as u64, + sqlite_transient(), + libsqlite3_sys::SQLITE_UTF8 as u8, + ) + } + SqliteValueTag::Bytes => { + let bytes = value.payload.bytes.as_slice(); + libsqlite3_sys::sqlite3_bind_blob64( + stmt, + index, + bytes.as_ptr() as *const c_void, + bytes.len() as u64, + sqlite_transient(), + ) + } + SqliteValueTag::Null => libsqlite3_sys::sqlite3_bind_null(stmt, index), + } +} + +fn sqlite_bind_all( + stmt: &mut SqliteStatement, + bindings: &[SqliteBindings], + roc_host: &RocHost, +) -> SqliteHostBindResult { + // Clear old bindings so callers must supply every parameter each time. + let cleared = unsafe { libsqlite3_sys::sqlite3_clear_bindings(stmt.stmt) }; + if cleared != libsqlite3_sys::SQLITE_OK { + return try_sqlite_unit_err(sqlite_err_from_stmt(stmt, cleared, roc_host)); + } + + for binding in bindings { + let name = match CString::new(binding.name.as_str()) { + Ok(name) => name, + Err(_) => { + return try_sqlite_unit_err(sqlite_error( + libsqlite3_sys::SQLITE_ERROR, + "binding name contained an interior nul byte", + roc_host, + )); + } + }; + let index = + unsafe { libsqlite3_sys::sqlite3_bind_parameter_index(stmt.stmt, name.as_ptr()) }; + if index == 0 { + return try_sqlite_unit_err(sqlite_error( + libsqlite3_sys::SQLITE_ERROR, + &format!("unknown parameter: {}", binding.name.as_str()), + roc_host, + )); + } + let err = unsafe { sqlite_bind_one(stmt.stmt, index, &binding.value) }; + if err != libsqlite3_sys::SQLITE_OK { + return try_sqlite_unit_err(sqlite_err_from_stmt(stmt, err, roc_host)); + } + } + + try_sqlite_unit_ok() +} + +#[no_mangle] +pub extern "C" fn hosted_sqlite_prepare(path: RocStr, query: RocStr) -> SqliteHostPrepareResult { + let roc_host = roc_host(); + let path_string = path.as_str().to_owned(); + let query_string = query.as_str().to_owned(); + path.decref(roc_host); + query.decref(roc_host); + + let connection = match sqlite_get_connection(&path_string) { + Ok(connection) => connection, + Err((code, message)) => { + return try_sqlite_prepare_err(sqlite_error(code, &message, roc_host)); + } + }; + + let mut stmt: *mut libsqlite3_sys::sqlite3_stmt = core::ptr::null_mut(); + let err = unsafe { + libsqlite3_sys::sqlite3_prepare_v2( + connection, + query_string.as_ptr() as *const c_char, + query_string.len() as c_int, + &mut stmt, + core::ptr::null_mut(), + ) + }; + if err != libsqlite3_sys::SQLITE_OK { + let message = sqlite_errmsg(connection, err); + return try_sqlite_prepare_err(sqlite_error(err, &message, roc_host)); + } + + let handle = box_sqlite_stmt(SqliteStatement { connection, stmt }, roc_host); + try_sqlite_prepare_ok(handle) +} + +#[no_mangle] +pub extern "C" fn hosted_sqlite_bind( + handle: *mut u64, + bindings: RocList, +) -> SqliteHostBindResult { + let roc_host = roc_host(); + let result = { + let stmt = unsafe { sqlite_stmt_ref(handle) }; + sqlite_bind_all(stmt, bindings.as_slice(), roc_host) + }; + for binding in bindings.as_slice() { + decref_anon_struct93(*binding, roc_host); + } + bindings.decref(roc_host); + release_sqlite_stmt(handle, roc_host); + result +} + +#[no_mangle] +pub extern "C" fn hosted_sqlite_columns(handle: *mut u64) -> RocList { + let roc_host = roc_host(); + let stmt = unsafe { sqlite_stmt_ref(handle) }; + let count = unsafe { libsqlite3_sys::sqlite3_column_count(stmt.stmt) }.max(0) as usize; + let list = RocList::::allocate(count, roc_host); + for index in 0..count { + let name = unsafe { + let raw = libsqlite3_sys::sqlite3_column_name(stmt.stmt, index as c_int); + if raw.is_null() { + RocStr::from_str("", roc_host) + } else { + RocStr::from_str(CStr::from_ptr(raw).to_string_lossy().as_ref(), roc_host) + } + }; + unsafe { + list.elements.add(index).write(name); + } + } + release_sqlite_stmt(handle, roc_host); + list +} + +#[no_mangle] +pub extern "C" fn hosted_sqlite_column_value( + handle: *mut u64, + i: u64, +) -> SqliteHostColumnValueResult { + let roc_host = roc_host(); + let result = { + let stmt = unsafe { sqlite_stmt_ref(handle) }; + let count = unsafe { libsqlite3_sys::sqlite3_column_count(stmt.stmt) }.max(0) as u64; + if i >= count { + try_sqlite_value_err(sqlite_error( + libsqlite3_sys::SQLITE_ERROR, + &format!("column index out of range: {} of {}", i, count), + roc_host, + )) + } else { + let index = i as c_int; + let value = unsafe { + match libsqlite3_sys::sqlite3_column_type(stmt.stmt, index) { + libsqlite3_sys::SQLITE_INTEGER => { + sqlite_value_integer(libsqlite3_sys::sqlite3_column_int64(stmt.stmt, index)) + } + libsqlite3_sys::SQLITE_FLOAT => { + sqlite_value_real(libsqlite3_sys::sqlite3_column_double(stmt.stmt, index)) + } + libsqlite3_sys::SQLITE_TEXT => { + let text = libsqlite3_sys::sqlite3_column_text(stmt.stmt, index); + let len = libsqlite3_sys::sqlite3_column_bytes(stmt.stmt, index).max(0) + as usize; + let slice = if text.is_null() { + &[][..] + } else { + std::slice::from_raw_parts(text, len) + }; + sqlite_value_string(RocStr::from_str( + String::from_utf8_lossy(slice).as_ref(), + roc_host, + )) + } + libsqlite3_sys::SQLITE_BLOB => { + let blob = libsqlite3_sys::sqlite3_column_blob(stmt.stmt, index) as *const u8; + let len = libsqlite3_sys::sqlite3_column_bytes(stmt.stmt, index).max(0) + as usize; + let slice = if blob.is_null() { + &[][..] + } else { + std::slice::from_raw_parts(blob, len) + }; + sqlite_value_bytes(RocListWith::::from_slice(slice, roc_host)) + } + _ => sqlite_value_null(), + } + }; + try_sqlite_value_ok(value) + } + }; + release_sqlite_stmt(handle, roc_host); + result +} + +#[no_mangle] +pub extern "C" fn hosted_sqlite_step(handle: *mut u64) -> SqliteHostStepResult { + let roc_host = roc_host(); + let result = { + let stmt = unsafe { sqlite_stmt_ref(handle) }; + let err = unsafe { libsqlite3_sys::sqlite3_step(stmt.stmt) }; + if err == libsqlite3_sys::SQLITE_ROW { + try_sqlite_step_ok(true) + } else if err == libsqlite3_sys::SQLITE_DONE { + try_sqlite_step_ok(false) + } else { + try_sqlite_step_err(sqlite_err_from_stmt(stmt, err, roc_host)) + } + }; + release_sqlite_stmt(handle, roc_host); + result +} + +#[no_mangle] +pub extern "C" fn hosted_sqlite_reset(handle: *mut u64) -> SqliteHostBindResult { + let roc_host = roc_host(); + let result = { + let stmt = unsafe { sqlite_stmt_ref(handle) }; + let err = unsafe { libsqlite3_sys::sqlite3_reset(stmt.stmt) }; + if err == libsqlite3_sys::SQLITE_OK { + try_sqlite_unit_ok() + } else { + try_sqlite_unit_err(sqlite_err_from_stmt(stmt, err, roc_host)) + } + }; + release_sqlite_stmt(handle, roc_host); + result +} + +// ============================================================================ +// TCP +// +// `Tcp.Stream` (a `Box(U64)`) is represented by the generated glue as `*mut u64`: +// a boxed u64 holding a raw `*mut BufReader`. The box is refcounted +// with `allocate_box`/`decref_box_with`; closing the socket happens in +// `drop_tcp_stream` when the last reference is released. Each host fn that takes +// a handle calls `release_tcp_stream` before returning to balance the incref Roc +// performs when the stream stays live. +// +// Errors cross the boundary as a `RocStr` carrying either "ErrorKind::" +// (mapped back to a tag union in Tcp.roc) or "UnexpectedEof"; the Roc side parses +// them into `ConnectErr`/`StreamErr`. +// ---------------------------------------------------------------------------- + +const TCP_STREAM_BOX_ALIGN: usize = core::mem::align_of::(); + +fn box_tcp_stream(stream: BufReader, roc_host: &RocHost) -> *mut u64 { + let raw: *mut BufReader = Box::into_raw(Box::new(stream)); + let boxed = allocate_box( + core::mem::size_of::(), + TCP_STREAM_BOX_ALIGN, + false, + roc_host, + ); + unsafe { + *(boxed as *mut u64) = raw as u64; + } + boxed as *mut u64 +} + +unsafe fn tcp_stream_ref<'a>(handle: *mut u64) -> &'a mut BufReader { + &mut *(*handle as *mut BufReader) +} + +extern "C" fn drop_tcp_stream(data_ptr: *mut c_void, _roc_host: *mut RocHost) { + unsafe { + let raw = *(data_ptr as *mut u64) as *mut BufReader; + if !raw.is_null() { + drop(Box::from_raw(raw)); + } + } +} + +fn release_tcp_stream(handle: *mut u64, roc_host: &RocHost) { + decref_box_with( + handle as RocBox, + TCP_STREAM_BOX_ALIGN, + // The boxed payload is a raw `u64` (a pointer to our BufReader), not a + // Roc-refcounted value — must match the `false` passed to `allocate_box`. + false, + Some(drop_tcp_stream), + roc_host, + ); +} + +fn to_tcp_connect_err(err: io::Error, roc_host: &RocHost) -> RocStr { + let message = match err.kind() { + io::ErrorKind::PermissionDenied => "ErrorKind::PermissionDenied".to_string(), + io::ErrorKind::AddrInUse => "ErrorKind::AddrInUse".to_string(), + io::ErrorKind::AddrNotAvailable => "ErrorKind::AddrNotAvailable".to_string(), + io::ErrorKind::ConnectionRefused => "ErrorKind::ConnectionRefused".to_string(), + io::ErrorKind::Interrupted => "ErrorKind::Interrupted".to_string(), + io::ErrorKind::TimedOut => "ErrorKind::TimedOut".to_string(), + io::ErrorKind::Unsupported => "ErrorKind::Unsupported".to_string(), + other => format!("{:?}", other), + }; + RocStr::from_str(&message, roc_host) +} + +fn to_tcp_stream_err(err: io::Error, roc_host: &RocHost) -> RocStr { + let message = match err.kind() { + io::ErrorKind::PermissionDenied => "ErrorKind::PermissionDenied".to_string(), + io::ErrorKind::ConnectionRefused => "ErrorKind::ConnectionRefused".to_string(), + io::ErrorKind::ConnectionReset => "ErrorKind::ConnectionReset".to_string(), + io::ErrorKind::Interrupted => "ErrorKind::Interrupted".to_string(), + io::ErrorKind::OutOfMemory => "ErrorKind::OutOfMemory".to_string(), + io::ErrorKind::BrokenPipe => "ErrorKind::BrokenPipe".to_string(), + other => format!("{:?}", other), + }; + RocStr::from_str(&message, roc_host) +} + +// `BufRead::read_until` ported from `roc_file::read_until`, accumulating into a +// plain Vec (the delimiter is included as the last byte when found). +fn tcp_read_until_impl(stream: &mut BufReader, delim: u8) -> io::Result> { + let mut buffer = Vec::new(); + loop { + let (done, used) = { + let available = match stream.fill_buf() { + Ok(n) => n, + Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + }; + match available.iter().position(|&b| b == delim) { + Some(i) => { + buffer.extend_from_slice(&available[..=i]); + (true, i + 1) + } + None => { + buffer.extend_from_slice(available); + (false, available.len()) + } + } + }; + stream.consume(used); + if done || used == 0 { + return Ok(buffer); + } + } +} + +fn try_tcp_connect_ok(handle: *mut u64) -> TcpHostConnectResult { + TcpHostConnectResult { + payload: TcpHostConnectResultPayload { + ok: ManuallyDrop::new(handle), + }, + tag: TcpHostConnectResultTag::Ok, + } +} + +fn try_tcp_connect_err(error: RocStr) -> TcpHostConnectResult { + TcpHostConnectResult { + payload: TcpHostConnectResultPayload { + err: ManuallyDrop::new(error), + }, + tag: TcpHostConnectResultTag::Err, + } +} + +// The three read host fns share an identical result layout (`Try(List U8, Str)`). +fn try_tcp_read_ok(bytes: RocListWith) -> TcpHostReadUpToResult { + TcpHostReadUpToResult { + payload: TcpHostReadUpToResultPayload { + ok: ManuallyDrop::new(bytes), + }, + tag: TcpHostReadUpToResultTag::Ok, + } +} + +fn try_tcp_read_err(error: RocStr) -> TcpHostReadUpToResult { + TcpHostReadUpToResult { + payload: TcpHostReadUpToResultPayload { + err: ManuallyDrop::new(error), + }, + tag: TcpHostReadUpToResultTag::Err, + } +} + +fn try_tcp_write_ok() -> TcpHostWriteResult { + TcpHostWriteResult { + payload: TcpHostWriteResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: TcpHostWriteResultTag::Ok, + } +} + +fn try_tcp_write_err(error: RocStr) -> TcpHostWriteResult { + TcpHostWriteResult { + payload: TcpHostWriteResultPayload { + err: ManuallyDrop::new(error), + }, + tag: TcpHostWriteResultTag::Err, + } +} + +#[no_mangle] +pub extern "C" fn hosted_tcp_connect(host: RocStr, port: u16) -> TcpHostConnectResult { + let roc_host = roc_host(); + let host_string = host.as_str().to_owned(); + host.decref(roc_host); + + match TcpStream::connect((host_string.as_str(), port)) { + Ok(stream) => { + let handle = box_tcp_stream(BufReader::new(stream), roc_host); + try_tcp_connect_ok(handle) + } + Err(err) => try_tcp_connect_err(to_tcp_connect_err(err, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_tcp_read_up_to( + handle: *mut u64, + bytes_to_read: u64, +) -> TcpHostReadUpToResult { + let roc_host = roc_host(); + let result = { + let stream = unsafe { tcp_stream_ref(handle) }; + let mut chunk = stream.take(bytes_to_read); + match chunk.fill_buf() { + Ok(received) => { + let received = received.to_vec(); + stream.consume(received.len()); + try_tcp_read_ok(RocListWith::::from_slice(&received, roc_host)) + } + Err(err) => try_tcp_read_err(to_tcp_stream_err(err, roc_host)), + } + }; + release_tcp_stream(handle, roc_host); + result +} + +#[no_mangle] +pub extern "C" fn hosted_tcp_read_exactly( + handle: *mut u64, + bytes_to_read: u64, +) -> TcpHostReadExactlyResult { + let roc_host = roc_host(); + let result = { + let stream = unsafe { tcp_stream_ref(handle) }; + let mut buffer = Vec::with_capacity(bytes_to_read as usize); + let mut chunk = stream.take(bytes_to_read); + match chunk.read_to_end(&mut buffer) { + Ok(read) => { + if (read as u64) < bytes_to_read { + try_tcp_read_err(RocStr::from_str("UnexpectedEof", roc_host)) + } else { + try_tcp_read_ok(RocListWith::::from_slice(&buffer, roc_host)) + } + } + Err(err) => try_tcp_read_err(to_tcp_stream_err(err, roc_host)), + } + }; + release_tcp_stream(handle, roc_host); + result +} + +#[no_mangle] +pub extern "C" fn hosted_tcp_read_until(handle: *mut u64, byte: u8) -> TcpHostReadUntilResult { + let roc_host = roc_host(); + let result = { + let stream = unsafe { tcp_stream_ref(handle) }; + match tcp_read_until_impl(stream, byte) { + Ok(buffer) => try_tcp_read_ok(RocListWith::::from_slice(&buffer, roc_host)), + Err(err) => try_tcp_read_err(to_tcp_stream_err(err, roc_host)), + } + }; + release_tcp_stream(handle, roc_host); + result +} + +#[no_mangle] +pub extern "C" fn hosted_tcp_write( + handle: *mut u64, + msg: RocListWith, +) -> TcpHostWriteResult { + let roc_host = roc_host(); + let result = { + let stream = unsafe { tcp_stream_ref(handle) }; + match stream.get_mut().write_all(msg.as_slice()) { + Ok(()) => try_tcp_write_ok(), + Err(err) => try_tcp_write_err(to_tcp_stream_err(err, roc_host)), + } + }; + msg.decref(roc_host); + release_tcp_stream(handle, roc_host); + result +} + +// ============================================================================ +// HTTP +// +// A single host effect, `hosted_http_send_request`, takes a fully-marshalled +// request record and returns a response record. Requests run on a thread-local +// current-thread tokio runtime driving a hyper client over a rustls (ring) +// TLS connector seeded with the system's native root certificates. +// +// Transport failures are surfaced to Roc as reserved status+body sentinels +// (matching the checks in Http.roc's `send!`): +// * 408 + "Timeout" -> request exceeded its timeout +// * 500 + "NetworkError" -> could not initialise the TLS connector +// * 500 + "BadBody" -> response body could not be collected +// * 500 + "OTHER ERROR\n…" -> any other transport/build error (detail follows) +// ---------------------------------------------------------------------------- + +// The generated glue names the request/response records by anonymous-struct +// number; alias them to the stable semantic names (the response also has the +// generator's stable `HttpHostSendRequest` alias). +type HttpResponse = HttpHostSendRequest; +type HttpHeader = AnonStruct57; + +thread_local! { + static TOKIO_RUNTIME: tokio::runtime::Runtime = tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + .expect("failed to build tokio runtime"); +} + +// Numeric method tags must match `to_host_method` in platform/Http.roc. +fn as_hyper_method(method: u64, method_ext: &str) -> Option { + match method { + 0 => Some(hyper::Method::CONNECT), + 1 => Some(hyper::Method::DELETE), + 2 => hyper::Method::from_bytes(method_ext.as_bytes()).ok(), + 3 => Some(hyper::Method::GET), + 4 => Some(hyper::Method::HEAD), + 5 => Some(hyper::Method::OPTIONS), + 6 => Some(hyper::Method::PATCH), + 7 => Some(hyper::Method::POST), + 8 => Some(hyper::Method::PUT), + 9 => Some(hyper::Method::TRACE), + _ => None, + } +} + +fn http_sentinel_response(status: u16, body: &[u8], roc_host: &RocHost) -> HttpResponse { + HttpResponse { + body: RocListWith::::from_slice(body, roc_host), + headers: RocList::empty(), + status, + } +} + +fn build_hyper_request( + args: &HttpHostSendRequestArgs, +) -> Result>, String> { + let method = as_hyper_method(args.method, args.method_ext.as_str()) + .ok_or_else(|| "invalid HTTP method".to_string())?; + let mut builder = hyper::Request::builder() + .method(method) + .uri(args.uri.as_str()); + + // Default to text/plain unless the caller already set a Content-Type. + let mut has_content_type = false; + for header in args.headers.as_slice() { + builder = builder.header(header.name.as_str(), header.value.as_str()); + if header.name.as_str().eq_ignore_ascii_case("Content-Type") { + has_content_type = true; + } + } + if !has_content_type { + builder = builder.header("Content-Type", "text/plain"); + } + + let body = http_body_util::Full::new(bytes::Bytes::from(args.body.as_slice().to_vec())); + builder.body(body).map_err(|err| err.to_string()) +} + +fn build_roc_headers(pairs: &[(String, String)], roc_host: &RocHost) -> RocList { + let list = RocList::::allocate(pairs.len(), roc_host); + for (index, (name, value)) in pairs.iter().enumerate() { + let header = HttpHeader { + name: RocStr::from_str(name, roc_host), + value: RocStr::from_str(value, roc_host), + }; + unsafe { + list.elements.add(index).write(header); + } + } + list +} + +async fn async_send_request( + request: hyper::Request>, + roc_host: &RocHost, +) -> HttpResponse { + use http_body_util::BodyExt; + use hyper_rustls::HttpsConnectorBuilder; + use hyper_util::client::legacy::Client; + use hyper_util::rt::TokioExecutor; + + let https = match HttpsConnectorBuilder::new().with_native_roots() { + Ok(builder) => builder.https_or_http().enable_http1().build(), + Err(_) => return http_sentinel_response(500, b"NetworkError", roc_host), + }; + + let client: Client<_, http_body_util::Full> = + Client::builder(TokioExecutor::new()).build(https); + + match client.request(request).await { + Ok(response) => { + let status = response.status().as_u16(); + let pairs: Vec<(String, String)> = response + .headers() + .iter() + .map(|(name, value)| { + ( + name.as_str().to_string(), + value.to_str().unwrap_or_default().to_string(), + ) + }) + .collect(); + + match response.into_body().collect().await { + Ok(collected) => { + let bytes = collected.to_bytes(); + HttpResponse { + body: RocListWith::::from_slice(&bytes, roc_host), + headers: build_roc_headers(&pairs, roc_host), + status, + } + } + Err(_) => http_sentinel_response(500, b"BadBody", roc_host), + } + } + Err(err) => { + let detail = format!("OTHER ERROR\n{}", err); + http_sentinel_response(500, detail.as_bytes(), roc_host) + } + } +} + +#[no_mangle] +pub extern "C" fn hosted_http_send_request(args: HttpHostSendRequestArgs) -> HttpResponse { + let roc_host = roc_host(); + let timeout_ms = args.timeout_ms; + + // Build the hyper request from the borrowed args, then release the owned + // Roc values (the request has copied everything it needs). + let request_result = build_hyper_request(&args); + args.body.decref(roc_host); + for header in args.headers.as_slice() { + decref_anon_struct57(*header, roc_host); + } + args.headers.decref(roc_host); + args.method_ext.decref(roc_host); + args.uri.decref(roc_host); + + let request = match request_result { + Ok(request) => request, + Err(err) => { + return http_sentinel_response( + 500, + format!("OTHER ERROR\n{}", err).as_bytes(), + roc_host, + ) + } + }; + + TOKIO_RUNTIME.with(|rt| { + if timeout_ms > 0 { + rt.block_on(async { + match tokio::time::timeout( + std::time::Duration::from_millis(timeout_ms), + async_send_request(request, roc_host), + ) + .await + { + Ok(response) => response, + Err(_) => http_sentinel_response(408, b"Timeout", roc_host), + } + }) + } else { + rt.block_on(async_send_request(request, roc_host)) + } + }) +} + +extern "C" { + fn roc_main(args: RocList) -> i32; +} + +static DEBUG_OR_EXPECT_CALLED: AtomicBool = AtomicBool::new(false); +static mut ROC_HOST: *mut RocHost = core::ptr::null_mut(); + +fn set_roc_host(roc_host: *mut RocHost) { + unsafe { + ROC_HOST = roc_host; + } +} + +fn roc_host_ptr() -> *mut RocHost { + unsafe { + if ROC_HOST.is_null() { + eprintln!("roc host error: RocHost not initialized"); + std::process::exit(1); + } + ROC_HOST + } +} + +fn roc_host() -> &'static RocHost { + unsafe { &*roc_host_ptr() } +} + +macro_rules! define_common_io_err { + ($from_io:ident, $other:ident, $ty:ident, $tag:ident, $payload:ident) => { + fn $other(message: &str, roc_host: &RocHost) -> $ty { + $ty { + payload: $payload { + other: ManuallyDrop::new(RocStr::from_str(message, roc_host)), + }, + tag: $tag::Other, + } + } + + fn $from_io(error: &io::Error, roc_host: &RocHost) -> $ty { + match error.kind() { + io::ErrorKind::AlreadyExists => $ty { + payload: $payload { already_exists: [] }, + tag: $tag::AlreadyExists, + }, + io::ErrorKind::BrokenPipe => $ty { + payload: $payload { broken_pipe: [] }, + tag: $tag::BrokenPipe, + }, + io::ErrorKind::Interrupted => $ty { + payload: $payload { interrupted: [] }, + tag: $tag::Interrupted, + }, + io::ErrorKind::NotFound => $ty { + payload: $payload { not_found: [] }, + tag: $tag::NotFound, + }, + io::ErrorKind::OutOfMemory => $ty { + payload: $payload { out_of_memory: [] }, + tag: $tag::OutOfMemory, + }, + io::ErrorKind::PermissionDenied => $ty { + payload: $payload { + permission_denied: [], + }, + tag: $tag::PermissionDenied, + }, + io::ErrorKind::Unsupported => $ty { + payload: $payload { unsupported: [] }, + tag: $tag::Unsupported, + }, + _ => $other(&error.to_string(), roc_host), + } + } + }; +} + +define_common_io_err!( + cmd_io_err_from_io, + cmd_io_err_other, + CmdIOErr, + CmdIOErrTag, + CmdIOErrPayload +); +define_common_io_err!( + dir_io_err_from_io, + dir_io_err_other, + DirIOErr, + DirIOErrTag, + DirIOErrPayload +); +define_common_io_err!( + file_io_err_from_io, + file_io_err_other, + FileIOErr, + FileIOErrTag, + FileIOErrPayload +); +define_common_io_err!( + path_io_err_from_io, + path_io_err_other, + PathIOErr, + PathIOErrTag, + PathIOErrPayload +); +define_common_io_err!( + random_io_err_from_io, + random_io_err_other, + RandomIOErr, + RandomIOErrTag, + RandomIOErrPayload +); +define_common_io_err!( + stderr_io_err_from_io, + stderr_io_err_other, + StderrIOErr, + StderrIOErrTag, + StderrIOErrPayload +); +define_common_io_err!( + stdin_io_err_from_io, + stdin_io_err_other, + StdinIOErr, + StdinIOErrTag, + StdinIOErrPayload +); +define_common_io_err!( + stdout_io_err_from_io, + stdout_io_err_other, + StdoutIOErr, + StdoutIOErrTag, + StdoutIOErrPayload +); + +fn decref_roc_str_list(list: &RocList, roc_host: &RocHost) { + for item in list.as_slice() { + item.decref(roc_host); + } + list.decref(roc_host); +} + +fn decref_host_cmd_arg(cmd: &Cmd, roc_host: &RocHost) { + decref_roc_str_list(&cmd.args, roc_host); + decref_roc_str_list(&cmd.envs, roc_host); + cmd.program.decref(roc_host); +} + +fn cmd_to_std(cmd: &Cmd) -> std::process::Command { + let mut std_cmd = std::process::Command::new(cmd.program.as_str()); + + for arg in cmd.args.as_slice() { + std_cmd.arg(arg.as_str()); + } + + if cmd.clear_envs { + std_cmd.env_clear(); + } + + for chunk in cmd.envs.as_slice().chunks(2) { + if let [key, value] = chunk { + std_cmd.env(key.as_str(), value.as_str()); + } + } + + std_cmd +} + +fn try_cmd_exit_ok(value: i32) -> CmdExitResult { + CmdExitResult { + payload: CmdExitResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: CmdExitResultTag::Ok, + } +} + +fn try_cmd_exit_err(error: CmdIOErr) -> CmdExitResult { + CmdExitResult { + payload: CmdExitResultPayload { + err: ManuallyDrop::new(error), + }, + tag: CmdExitResultTag::Err, + } +} + +fn try_cmd_output_ok(value: CmdOutputSuccess) -> CmdOutputResult { + CmdOutputResult { + payload: CmdOutputResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: CmdOutputResultTag::Ok, + } +} + +fn try_cmd_output_err(error: CmdOutputFailureResult) -> CmdOutputResult { + CmdOutputResult { + payload: CmdOutputResultPayload { + err: ManuallyDrop::new(error), + }, + tag: CmdOutputResultTag::Err, + } +} + +fn try_cmd_output_failure_ok(value: CmdOutputFailure) -> CmdOutputFailureResult { + CmdOutputFailureResult { + payload: CmdOutputFailureResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: CmdOutputFailureResultTag::Ok, + } +} + +fn try_cmd_output_failure_err(error: CmdIOErr) -> CmdOutputFailureResult { + CmdOutputFailureResult { + payload: CmdOutputFailureResultPayload { + err: ManuallyDrop::new(error), + }, + tag: CmdOutputFailureResultTag::Err, + } +} + +fn try_dir_unit_ok() -> DirUnitResult { + DirUnitResult { + payload: DirUnitResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: DirUnitResultTag::Ok, + } +} + +fn try_dir_unit_err(error: DirIOErr) -> DirUnitResult { + DirUnitResult { + payload: DirUnitResultPayload { + err: ManuallyDrop::new(error), + }, + tag: DirUnitResultTag::Err, + } +} + +fn try_dir_list_ok(value: RocList) -> DirListResult { + DirListResult { + payload: DirListResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: DirListResultTag::Ok, + } +} + +fn try_dir_list_err(error: DirIOErr) -> DirListResult { + DirListResult { + payload: DirListResultPayload { + err: ManuallyDrop::new(error), + }, + tag: DirListResultTag::Err, + } +} + +fn try_env_str_ok(value: RocStr) -> EnvVarResult { + EnvVarResult { + payload: EnvVarResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: EnvVarResultTag::Ok, + } +} + +fn try_env_str_err(error: RocStr) -> EnvVarResult { + EnvVarResult { + payload: EnvVarResultPayload { + err: ManuallyDrop::new(error), + }, + tag: EnvVarResultTag::Err, + } +} + +fn try_env_cwd_ok(value: RocStr) -> EnvCwdResult { + EnvCwdResult { + payload: EnvCwdResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: EnvCwdResultTag::Ok, + } +} + +fn try_env_cwd_err() -> EnvCwdResult { + EnvCwdResult { + payload: EnvCwdResultPayload { + err: ManuallyDrop::new(core::ptr::null_mut()), + }, + tag: EnvCwdResultTag::Err, + } +} + +fn try_env_exe_path_ok(value: RocStr) -> EnvExePathResult { + EnvExePathResult { + payload: EnvExePathResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: EnvExePathResultTag::Ok, + } +} + +fn try_env_exe_path_err() -> EnvExePathResult { + EnvExePathResult { + payload: EnvExePathResultPayload { + err: ManuallyDrop::new(core::ptr::null_mut()), + }, + tag: EnvExePathResultTag::Err, + } +} + +fn try_file_bytes_ok(value: RocListWith) -> FileBytesResult { + FileBytesResult { + payload: FileBytesResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: FileBytesResultTag::Ok, + } +} + +fn try_file_bytes_err(error: FileIOErr) -> FileBytesResult { + FileBytesResult { + payload: FileBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileBytesResultTag::Err, + } +} + +fn try_file_write_bytes_ok() -> FileWriteBytesResult { + FileWriteBytesResult { + payload: FileWriteBytesResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: FileWriteBytesResultTag::Ok, + } +} + +fn try_file_write_bytes_err(error: FileIOErr) -> FileWriteBytesResult { + FileWriteBytesResult { + payload: FileWriteBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileWriteBytesResultTag::Err, + } +} + +fn try_file_str_ok(value: RocStr) -> FileStrResult { + FileStrResult { + payload: FileStrResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: FileStrResultTag::Ok, + } +} + +fn try_file_str_err(error: FileIOErr) -> FileStrResult { + FileStrResult { + payload: FileStrResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileStrResultTag::Err, + } +} + +fn try_file_write_utf8_ok() -> FileWriteUtf8Result { + FileWriteUtf8Result { + payload: FileWriteUtf8ResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: FileWriteUtf8ResultTag::Ok, + } +} + +fn try_file_write_utf8_err(error: FileIOErr) -> FileWriteUtf8Result { + FileWriteUtf8Result { + payload: FileWriteUtf8ResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileWriteUtf8ResultTag::Err, + } +} + +fn try_file_delete_ok() -> FileDeleteResult { + FileDeleteResult { + payload: FileDeleteResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: FileDeleteResultTag::Ok, + } +} + +fn try_file_delete_err(error: FileIOErr) -> FileDeleteResult { + FileDeleteResult { + payload: FileDeleteResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileDeleteResultTag::Err, + } +} + +fn try_file_size_ok(value: u64) -> FileSizeResult { + FileSizeResult { + payload: FileSizeResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: FileSizeResultTag::Ok, + } +} + +fn try_file_size_err(error: FileIOErr) -> FileSizeResult { + FileSizeResult { + payload: FileSizeResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileSizeResultTag::Err, + } +} + +fn try_file_bool_ok(value: bool) -> FileBoolResult { + FileBoolResult { + payload: FileBoolResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: FileBoolResultTag::Ok, + } +} + +fn try_file_bool_err(error: FileIOErr) -> FileBoolResult { + FileBoolResult { + payload: FileBoolResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileBoolResultTag::Err, + } +} + +fn try_file_time_ok(value: u128) -> FileTimeResult { + FileTimeResult { + payload: FileTimeResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: FileTimeResultTag::Ok, + } +} + +fn try_file_time_err(error: FileIOErr) -> FileTimeResult { + FileTimeResult { + payload: FileTimeResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileTimeResultTag::Err, + } +} + +fn try_locale_get_ok(value: RocStr) -> LocaleGetResult { + LocaleGetResult { + payload: LocaleGetResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: LocaleGetResultTag::Ok, + } +} + +fn try_path_type_ok(value: PathInfo) -> PathTypeResult { + PathTypeResult { + payload: PathTypeResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: PathTypeResultTag::Ok, + } +} + +fn try_path_type_err(error: PathIOErr) -> PathTypeResult { + PathTypeResult { + payload: PathTypeResultPayload { + err: ManuallyDrop::new(error), + }, + tag: PathTypeResultTag::Err, + } +} + +fn try_random_u64_ok(value: u64) -> RandomU64Result { + RandomU64Result { + payload: RandomU64ResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: RandomU64ResultTag::Ok, + } +} + +fn try_random_u64_err(error: RandomIOErr) -> RandomU64Result { + RandomU64Result { + payload: RandomU64ResultPayload { + err: ManuallyDrop::new(error), + }, + tag: RandomU64ResultTag::Err, + } +} + +fn try_random_u32_ok(value: u32) -> RandomU32Result { + RandomU32Result { + payload: RandomU32ResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: RandomU32ResultTag::Ok, + } +} + +fn try_random_u32_err(error: RandomIOErr) -> RandomU32Result { + RandomU32Result { + payload: RandomU32ResultPayload { + err: ManuallyDrop::new(error), + }, + tag: RandomU32ResultTag::Err, + } +} + +fn try_stderr_unit_ok() -> StderrUnitResult { + StderrUnitResult { + payload: StderrUnitResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: StderrUnitResultTag::Ok, + } +} + +fn try_stderr_unit_err(error: StderrIOErr) -> StderrUnitResult { + StderrUnitResult { + payload: StderrUnitResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StderrUnitResultTag::Err, + } +} + +fn try_stderr_bytes_ok() -> StderrBytesResult { + StderrBytesResult { + payload: StderrBytesResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: StderrBytesResultTag::Ok, + } +} + +fn try_stderr_bytes_err(error: StderrIOErr) -> StderrBytesResult { + StderrBytesResult { + payload: StderrBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StderrBytesResultTag::Err, + } +} + +fn stdin_line_eof_or_err_eof() -> StdinLineReadErr { + StdinLineReadErr { + payload: StdinLineReadErrPayload { end_of_file: [] }, + tag: StdinLineReadErrTag::EndOfFile, + } +} + +fn stdin_line_eof_or_err_io(error: StdinIOErr) -> StdinLineReadErr { + StdinLineReadErr { + payload: StdinLineReadErrPayload { + stdin_err: ManuallyDrop::new(error), + }, + tag: StdinLineReadErrTag::StdinErr, + } +} + +fn stdin_bytes_eof_or_err_eof() -> StdinBytesReadErr { + StdinBytesReadErr { + payload: StdinBytesReadErrPayload { end_of_file: [] }, + tag: StdinBytesReadErrTag::EndOfFile, + } +} + +fn stdin_bytes_eof_or_err_io(error: StdinIOErr) -> StdinBytesReadErr { + StdinBytesReadErr { + payload: StdinBytesReadErrPayload { + stdin_err: ManuallyDrop::new(error), + }, + tag: StdinBytesReadErrTag::StdinErr, + } +} + +fn try_stdin_line_ok(value: RocStr) -> StdinLineResult { + StdinLineResult { + payload: StdinLineResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: StdinLineResultTag::Ok, + } +} + +fn try_stdin_line_err(error: StdinLineReadErr) -> StdinLineResult { + StdinLineResult { + payload: StdinLineResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StdinLineResultTag::Err, + } +} + +fn try_stdin_bytes_ok(value: RocListWith) -> StdinBytesResult { + StdinBytesResult { + payload: StdinBytesResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: StdinBytesResultTag::Ok, + } +} + +fn try_stdin_bytes_err(error: StdinBytesReadErr) -> StdinBytesResult { + StdinBytesResult { + payload: StdinBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StdinBytesResultTag::Err, + } +} + +fn try_stdin_read_to_end_ok(value: RocListWith) -> StdinReadToEndResult { + StdinReadToEndResult { + payload: StdinReadToEndResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: StdinReadToEndResultTag::Ok, + } +} + +fn try_stdin_read_to_end_err(error: StdinIOErr) -> StdinReadToEndResult { + StdinReadToEndResult { + payload: StdinReadToEndResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StdinReadToEndResultTag::Err, + } +} + +fn try_stdout_unit_ok() -> StdoutUnitResult { + StdoutUnitResult { + payload: StdoutUnitResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: StdoutUnitResultTag::Ok, + } +} + +fn try_stdout_unit_err(error: StdoutIOErr) -> StdoutUnitResult { + StdoutUnitResult { + payload: StdoutUnitResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StdoutUnitResultTag::Err, + } +} + +fn try_stdout_bytes_ok() -> StdoutBytesResult { + StdoutBytesResult { + payload: StdoutBytesResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: StdoutBytesResultTag::Ok, + } +} + +fn try_stdout_bytes_err(error: StdoutIOErr) -> StdoutBytesResult { + StdoutBytesResult { + payload: StdoutBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StdoutBytesResultTag::Err, + } +} + +#[no_mangle] +pub extern "C" fn hosted_cmd_host_exec_exit_code(cmd: Cmd) -> CmdExitResult { + let roc_host = roc_host(); + let mut std_cmd = cmd_to_std(&cmd); + decref_host_cmd_arg(&cmd, roc_host); + + match std_cmd.status() { + Ok(status) => match status.code() { + Some(code) => try_cmd_exit_ok(code), + None => try_cmd_exit_err(cmd_io_err_other("Process was killed by signal", roc_host)), + }, + Err(error) => try_cmd_exit_err(cmd_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_cmd_host_exec_output(cmd: Cmd) -> CmdOutputResult { + let roc_host = roc_host(); + let mut std_cmd = cmd_to_std(&cmd); + decref_host_cmd_arg(&cmd, roc_host); + + match std_cmd.output() { + Ok(output) => { + let stdout_bytes = RocListWith::::from_slice(&output.stdout, roc_host); + let stderr_bytes = RocListWith::::from_slice(&output.stderr, roc_host); + + match output.status.code() { + Some(0) => try_cmd_output_ok(CmdOutputSuccess { + stderr_bytes, + stdout_bytes, + }), + Some(exit_code) => { + try_cmd_output_err(try_cmd_output_failure_ok(CmdOutputFailure { + stderr_bytes, + stdout_bytes, + exit_code, + })) + } + None => { + stdout_bytes.decref(roc_host); + stderr_bytes.decref(roc_host); + try_cmd_output_err(try_cmd_output_failure_err(cmd_io_err_other( + "Process was killed by signal", + roc_host, + ))) + } + } + } + Err(error) => try_cmd_output_err(try_cmd_output_failure_err(cmd_io_err_from_io( + &error, roc_host, + ))), + } +} + +fn path_from_roc_str(path: RocStr, roc_host: &RocHost) -> String { + let path_string = path.as_str().to_owned(); + path.decref(roc_host); + path_string +} + +#[no_mangle] +pub extern "C" fn hosted_dir_create(path: RocStr) -> DirUnitResult { + let roc_host = roc_host(); + match fs::create_dir(path_from_roc_str(path, roc_host)) { + Ok(()) => try_dir_unit_ok(), + Err(error) => try_dir_unit_err(dir_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_dir_create_all(path: RocStr) -> DirUnitResult { + let roc_host = roc_host(); + match fs::create_dir_all(path_from_roc_str(path, roc_host)) { + Ok(()) => try_dir_unit_ok(), + Err(error) => try_dir_unit_err(dir_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_dir_delete_all(path: RocStr) -> DirUnitResult { + let roc_host = roc_host(); + match fs::remove_dir_all(path_from_roc_str(path, roc_host)) { + Ok(()) => try_dir_unit_ok(), + Err(error) => try_dir_unit_err(dir_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_dir_delete_empty(path: RocStr) -> DirUnitResult { + let roc_host = roc_host(); + match fs::remove_dir(path_from_roc_str(path, roc_host)) { + Ok(()) => try_dir_unit_ok(), + Err(error) => try_dir_unit_err(dir_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_dir_list(path: RocStr) -> DirListResult { + let roc_host = roc_host(); + match fs::read_dir(path_from_roc_str(path, roc_host)) { + Ok(read_dir) => { + let entries: Vec = read_dir + .filter_map(|entry| { + entry + .ok() + .map(|entry| entry.path().to_string_lossy().into_owned()) + }) + .collect(); + let list = RocList::::allocate(entries.len(), roc_host); + for (index, entry) in entries.iter().enumerate() { + unsafe { + list.elements + .add(index) + .write(RocStr::from_str(entry, roc_host)); + } + } + try_dir_list_ok(list) + } + Err(error) => try_dir_list_err(dir_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_env_cwd() -> EnvCwdResult { + let roc_host = roc_host(); + match std::env::current_dir() { + Ok(path) => try_env_cwd_ok(RocStr::from_str(path.to_string_lossy().as_ref(), roc_host)), + Err(_) => try_env_cwd_err(), + } +} + +#[no_mangle] +pub extern "C" fn hosted_env_exe_path() -> EnvExePathResult { + let roc_host = roc_host(); + match std::env::current_exe() { + Ok(path) => { + try_env_exe_path_ok(RocStr::from_str(path.to_string_lossy().as_ref(), roc_host)) + } + Err(_) => try_env_exe_path_err(), + } +} + +#[no_mangle] +pub extern "C" fn hosted_env_temp_dir() -> RocStr { + let roc_host = roc_host(); + RocStr::from_str(std::env::temp_dir().to_string_lossy().as_ref(), roc_host) +} + +#[no_mangle] +pub extern "C" fn hosted_env_var(name: RocStr) -> EnvVarResult { + let roc_host = roc_host(); + let key = name.as_str().to_owned(); + match std::env::var_os(&key) { + Some(value) => { + name.decref(roc_host); + try_env_str_ok(RocStr::from_str(value.to_string_lossy().as_ref(), roc_host)) + } + None => try_env_str_err(name), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_delete(path: RocStr) -> FileDeleteResult { + let roc_host = roc_host(); + match fs::remove_file(path_from_roc_str(path, roc_host)) { + Ok(()) => try_file_delete_ok(), + Err(error) => try_file_delete_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_read_bytes(path: RocStr) -> FileBytesResult { + let roc_host = roc_host(); + match fs::read(path_from_roc_str(path, roc_host)) { + Ok(bytes) => try_file_bytes_ok(RocListWith::::from_slice(&bytes, roc_host)), + Err(error) => try_file_bytes_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_read_utf8(path: RocStr) -> FileStrResult { + let roc_host = roc_host(); + match fs::read_to_string(path_from_roc_str(path, roc_host)) { + Ok(content) => try_file_str_ok(RocStr::from_str(&content, roc_host)), + Err(error) => try_file_str_err(file_io_err_from_io(&error, roc_host)), + } +} + +fn file_metadata(path: RocStr, roc_host: &RocHost) -> io::Result { + fs::metadata(path_from_roc_str(path, roc_host)) +} + +#[no_mangle] +pub extern "C" fn hosted_file_size_in_bytes(path: RocStr) -> FileSizeResult { + let roc_host = roc_host(); + match file_metadata(path, roc_host) { + Ok(metadata) => try_file_size_ok(metadata.len()), + Err(error) => try_file_size_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[cfg(not(unix))] +fn unsupported_file_permission_error() -> io::Error { + io::Error::new( + io::ErrorKind::Unsupported, + "file permission checks are not implemented on this platform", + ) +} + +fn file_permission_bit(path: RocStr, roc_host: &RocHost, bit: u32) -> io::Result { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let metadata = file_metadata(path, roc_host)?; + Ok(metadata.permissions().mode() & bit != 0) + } + + #[cfg(not(unix))] + { + let _ = path_from_roc_str(path, roc_host); + let _ = bit; + Err(unsupported_file_permission_error()) + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_is_executable(path: RocStr) -> FileBoolResult { + let roc_host = roc_host(); + match file_permission_bit(path, roc_host, 0o111) { + Ok(value) => try_file_bool_ok(value), + Err(error) => try_file_bool_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_is_readable(path: RocStr) -> FileBoolResult { + let roc_host = roc_host(); + match file_permission_bit(path, roc_host, 0o400) { + Ok(value) => try_file_bool_ok(value), + Err(error) => try_file_bool_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_is_writable(path: RocStr) -> FileBoolResult { + let roc_host = roc_host(); + match file_permission_bit(path, roc_host, 0o200) { + Ok(value) => try_file_bool_ok(value), + Err(error) => try_file_bool_err(file_io_err_from_io(&error, roc_host)), + } +} + +fn nanos_since_epoch(time: std::time::SystemTime) -> io::Result { + time.duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .map_err(|error| io::Error::new(io::ErrorKind::Other, error.to_string())) +} + +fn file_time( + path: RocStr, + roc_host: &RocHost, + read_time: fn(&fs::Metadata) -> io::Result, +) -> io::Result { + let metadata = file_metadata(path, roc_host)?; + read_time(&metadata).and_then(nanos_since_epoch) +} + +#[no_mangle] +pub extern "C" fn hosted_file_time_accessed(path: RocStr) -> FileTimeResult { + let roc_host = roc_host(); + match file_time(path, roc_host, fs::Metadata::accessed) { + Ok(value) => try_file_time_ok(value), + Err(error) => try_file_time_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_time_created(path: RocStr) -> FileTimeResult { + let roc_host = roc_host(); + match file_time(path, roc_host, fs::Metadata::created) { + Ok(value) => try_file_time_ok(value), + Err(error) => try_file_time_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_time_modified(path: RocStr) -> FileTimeResult { + let roc_host = roc_host(); + match file_time(path, roc_host, fs::Metadata::modified) { + Ok(value) => try_file_time_ok(value), + Err(error) => try_file_time_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_write_bytes( + path: RocStr, + bytes: RocListWith, +) -> FileWriteBytesResult { + let roc_host = roc_host(); + let path_string = path_from_roc_str(path, roc_host); + let result = fs::write(path_string, bytes.as_slice()); + bytes.decref(roc_host); + + match result { + Ok(()) => try_file_write_bytes_ok(), + Err(error) => try_file_write_bytes_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_write_utf8(path: RocStr, content: RocStr) -> FileWriteUtf8Result { + let roc_host = roc_host(); + let path_string = path_from_roc_str(path, roc_host); + let content_string = content.as_str().to_owned(); + content.decref(roc_host); + + match fs::write(path_string, content_string) { + Ok(()) => try_file_write_utf8_ok(), + Err(error) => try_file_write_utf8_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[cfg(target_os = "macos")] +fn locale_from_env() -> Option { + for key in ["LC_ALL", "LC_CTYPE", "LANG"] { + if let Ok(value) = std::env::var(key) { + let trimmed = value.trim(); + if trimmed.is_empty() { + continue; + } + + let locale = trimmed + .split('.') + .next() + .unwrap_or(trimmed) + .split('@') + .next() + .unwrap_or(trimmed) + .trim(); + + if !locale.is_empty() { + return Some(locale.to_string()); + } + } + } + + None +} + +#[cfg(target_os = "macos")] +fn locale_get_string() -> String { + locale_from_env().unwrap_or_else(|| "en-US".to_string()) +} + +#[cfg(not(target_os = "macos"))] +fn locale_get_string() -> String { + sys_locale::get_locale().unwrap_or_else(|| "en-US".to_string()) +} + +#[cfg(target_os = "macos")] +fn locale_all_strings() -> Vec { + vec![locale_get_string()] +} + +#[cfg(not(target_os = "macos"))] +fn locale_all_strings() -> Vec { + let locales = sys_locale::get_locales().collect::>(); + if locales.is_empty() { + vec![locale_get_string()] + } else { + locales + } +} + +#[no_mangle] +pub extern "C" fn hosted_locale_all() -> RocList { + let roc_host = roc_host(); + let locales = locale_all_strings(); + let list = RocList::::allocate(locales.len(), roc_host); + + for (index, locale) in locales.iter().enumerate() { + unsafe { + list.elements + .add(index) + .write(RocStr::from_str(locale, roc_host)); + } + } + + list +} + +#[no_mangle] +pub extern "C" fn hosted_locale_get() -> LocaleGetResult { + let roc_host = roc_host(); + try_locale_get_ok(RocStr::from_str(&locale_get_string(), roc_host)) +} + +fn path_buf_from_roc_bytes( + bytes: RocListWith, + roc_host: &RocHost, +) -> std::path::PathBuf { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + + let path = std::ffi::OsStr::from_bytes(bytes.as_slice()).to_owned(); + bytes.decref(roc_host); + std::path::PathBuf::from(path) + } + + #[cfg(not(unix))] + { + let path = String::from_utf8_lossy(bytes.as_slice()).into_owned(); + bytes.decref(roc_host); + std::path::PathBuf::from(path) + } +} + +#[no_mangle] +pub extern "C" fn hosted_path_type(path: RocListWith) -> PathTypeResult { + let roc_host = roc_host(); + let path = path_buf_from_roc_bytes(path, roc_host); + + match path.symlink_metadata() { + Ok(metadata) => { + let file_type = metadata.file_type(); + try_path_type_ok(PathInfo { + is_dir: metadata.is_dir(), + is_file: metadata.is_file(), + is_sym_link: file_type.is_symlink(), + }) + } + Err(error) => try_path_type_err(path_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_random_seed_u32() -> RandomU32Result { + let roc_host = roc_host(); + let mut bytes = [0u8; 4]; + match getrandom::getrandom(&mut bytes) { + Ok(()) => try_random_u32_ok(u32::from_ne_bytes(bytes)), + Err(error) => { + let io_error = io::Error::new(io::ErrorKind::Other, error.to_string()); + try_random_u32_err(random_io_err_from_io(&io_error, roc_host)) + } + } +} + +#[no_mangle] +pub extern "C" fn hosted_random_seed_u64() -> RandomU64Result { + let roc_host = roc_host(); + let mut bytes = [0u8; 8]; + match getrandom::getrandom(&mut bytes) { + Ok(()) => try_random_u64_ok(u64::from_ne_bytes(bytes)), + Err(error) => { + let io_error = io::Error::new(io::ErrorKind::Other, error.to_string()); + try_random_u64_err(random_io_err_from_io(&io_error, roc_host)) + } + } +} + +#[no_mangle] +pub extern "C" fn hosted_sleep_millis(millis: u64) { + std::thread::sleep(std::time::Duration::from_millis(millis)); +} + +#[no_mangle] +pub extern "C" fn hosted_stderr_line(message: RocStr) -> StderrUnitResult { + let roc_host = roc_host(); + let result = { + let mut stderr = io::stderr().lock(); + writeln!(stderr, "{}", message.as_str()) + }; + message.decref(roc_host); + + match result { + Ok(()) => try_stderr_unit_ok(), + Err(error) => try_stderr_unit_err(stderr_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_stderr_write(message: RocStr) -> StderrUnitResult { + let roc_host = roc_host(); + let result = { + let mut stderr = io::stderr().lock(); + write!(stderr, "{}", message.as_str()).and_then(|()| stderr.flush()) + }; + message.decref(roc_host); + + match result { + Ok(()) => try_stderr_unit_ok(), + Err(error) => try_stderr_unit_err(stderr_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_stderr_write_bytes(bytes: RocListWith) -> StderrBytesResult { + let roc_host = roc_host(); + let result = { + let mut stderr = io::stderr().lock(); + stderr + .write_all(bytes.as_slice()) + .and_then(|()| stderr.flush()) + }; + bytes.decref(roc_host); + + match result { + Ok(()) => try_stderr_bytes_ok(), + Err(error) => try_stderr_bytes_err(stderr_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_stdin_line() -> StdinLineResult { + let roc_host = roc_host(); + let mut line = String::new(); + match io::stdin().lock().read_line(&mut line) { + Ok(0) => try_stdin_line_err(stdin_line_eof_or_err_eof()), + Ok(_) => { + let trimmed = line.trim_end_matches('\n').trim_end_matches('\r'); + try_stdin_line_ok(RocStr::from_str(trimmed, roc_host)) + } + Err(error) => try_stdin_line_err(stdin_line_eof_or_err_io(stdin_io_err_from_io( + &error, roc_host, + ))), + } +} + +#[no_mangle] +pub extern "C" fn hosted_stdin_bytes() -> StdinBytesResult { + let roc_host = roc_host(); + let mut buffer = [0u8; 16_384]; + match io::stdin().lock().read(&mut buffer) { + Ok(0) => try_stdin_bytes_err(stdin_bytes_eof_or_err_eof()), + Ok(bytes_read) => try_stdin_bytes_ok(RocListWith::::from_slice( + &buffer[..bytes_read], + roc_host, + )), + Err(error) => try_stdin_bytes_err(stdin_bytes_eof_or_err_io(stdin_io_err_from_io( + &error, roc_host, + ))), + } +} + +#[no_mangle] +pub extern "C" fn hosted_stdin_read_to_end() -> StdinReadToEndResult { + let roc_host = roc_host(); + let mut buffer = Vec::new(); + match io::stdin().lock().read_to_end(&mut buffer) { + Ok(_) => try_stdin_read_to_end_ok(RocListWith::::from_slice(&buffer, roc_host)), + Err(error) => try_stdin_read_to_end_err(stdin_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_stdout_line(message: RocStr) -> StdoutUnitResult { + let roc_host = roc_host(); + let result = { + let mut stdout = io::stdout().lock(); + writeln!(stdout, "{}", message.as_str()) + }; + message.decref(roc_host); + + match result { + Ok(()) => try_stdout_unit_ok(), + Err(error) => try_stdout_unit_err(stdout_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_stdout_write(message: RocStr) -> StdoutUnitResult { + let roc_host = roc_host(); + let result = { + let mut stdout = io::stdout().lock(); + write!(stdout, "{}", message.as_str()).and_then(|()| stdout.flush()) + }; + message.decref(roc_host); + + match result { + Ok(()) => try_stdout_unit_ok(), + Err(error) => try_stdout_unit_err(stdout_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_stdout_write_bytes(bytes: RocListWith) -> StdoutBytesResult { + let roc_host = roc_host(); + let result = { + let mut stdout = io::stdout().lock(); + stdout + .write_all(bytes.as_slice()) + .and_then(|()| stdout.flush()) + }; + bytes.decref(roc_host); + + match result { + Ok(()) => try_stdout_bytes_ok(), + Err(error) => try_stdout_bytes_err(stdout_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_tty_disable_raw_mode() { + let _ = disable_raw_mode(); +} + +#[no_mangle] +pub extern "C" fn hosted_tty_enable_raw_mode() { + let _ = enable_raw_mode(); +} + +#[no_mangle] +pub extern "C" fn hosted_utc_now() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("time went backwards") + .as_nanos() +} + +#[no_mangle] +pub extern "C" fn roc_alloc(length: usize, alignment: usize) -> *mut c_void { + DefaultAllocators::roc_alloc(roc_host_ptr(), length, alignment) +} + +#[no_mangle] +pub extern "C" fn roc_dealloc(ptr: *mut c_void, alignment: usize) { + DefaultAllocators::roc_dealloc(roc_host_ptr(), ptr, alignment); +} + +#[no_mangle] +pub extern "C" fn roc_realloc( + ptr: *mut c_void, + new_length: usize, + alignment: usize, +) -> *mut c_void { + DefaultAllocators::roc_realloc(roc_host_ptr(), ptr, new_length, alignment) +} + +#[no_mangle] +pub extern "C" fn roc_dbg(bytes: *const u8, len: usize) { + DEBUG_OR_EXPECT_CALLED.store(true, Ordering::Release); + DefaultHandlers::roc_dbg(roc_host_ptr(), bytes, len); +} + +#[no_mangle] +pub extern "C" fn roc_expect_failed(bytes: *const u8, len: usize) { + DEBUG_OR_EXPECT_CALLED.store(true, Ordering::Release); + DefaultHandlers::roc_expect_failed(roc_host_ptr(), bytes, len); +} + +#[no_mangle] +pub extern "C" fn roc_crashed(bytes: *const u8, len: usize) { + DefaultHandlers::roc_crashed(roc_host_ptr(), bytes, len); +} + +fn build_args_list(argc: i32, argv: *const *const c_char, roc_host: &RocHost) -> RocList { + if argc <= 0 || argv.is_null() { + return RocList::empty(); + } + + let list = RocList::::allocate(argc as usize, roc_host); + for index in 0..argc as isize { + unsafe { + let arg_ptr = *argv.offset(index); + if arg_ptr.is_null() { + break; + } + let arg = CStr::from_ptr(arg_ptr).to_string_lossy(); + list.elements + .offset(index) + .write(RocStr::from_str(&arg, roc_host)); + } + } + list +} + +#[cfg(not(test))] +#[no_mangle] +pub extern "C" fn main(argc: i32, argv: *const *const c_char) -> i32 { + rust_main(argc, argv) +} + +pub fn rust_main(argc: i32, argv: *const *const c_char) -> i32 { + let mut roc_host = make_roc_host(core::ptr::null_mut()); + set_roc_host(&mut roc_host); + + let args_list = build_args_list(argc, argv, &roc_host); + let mut exit_code = unsafe { roc_main(args_list) }; + + if DEBUG_OR_EXPECT_CALLED.load(Ordering::Acquire) && exit_code == 0 { + exit_code = 1; + } + + set_roc_host(core::ptr::null_mut()); + exit_code +} diff --git a/src/roc_platform_abi.rs b/src/roc_platform_abi.rs new file mode 100644 index 00000000..1cd492c6 --- /dev/null +++ b/src/roc_platform_abi.rs @@ -0,0 +1,4525 @@ +//! Roc Platform ABI +//! +//! This file defines the Rust interface for hosted functions in a Roc platform. +//! It is automatically generated by the Roc glue generator. +//! +//! Hosted argument ownership: +//! - Roc transfers ownership of refcounted arguments to the hosted function. +//! - The hosted function must decref owned refcounted arguments when done. +//! - If the host stores or returns an argument, it must retain or transfer ownership explicitly. +//! +//! Import this module from the platform host and implement the listed hosted symbols +//! with the exact natural C ABI signatures shown below. + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(dead_code)] + +use core::ffi::c_void; +use core::sync::atomic::{AtomicIsize, Ordering}; +use std::alloc::Layout; + +/// Runtime representation of an opaque `Box(T)` value. +pub type RocBox = *mut c_void; + +/// Host-internal allocator and diagnostic context used by helper functions in this file. +/// +/// Compiled Roc code does not receive this value. The real host ABI is the set of direct +/// linker symbols declared below (`roc_alloc`, hosted symbols, and provided entrypoints). +#[repr(C)] +pub struct RocHost { + pub env: *mut c_void, + pub roc_alloc: extern "C" fn(*mut RocHost, usize, usize) -> *mut c_void, + pub roc_dealloc: extern "C" fn(*mut RocHost, *mut c_void, usize), + pub roc_realloc: extern "C" fn(*mut RocHost, *mut c_void, usize, usize) -> *mut c_void, + pub roc_dbg: extern "C" fn(*mut RocHost, *const u8, usize), + pub roc_expect_failed: extern "C" fn(*mut RocHost, *const u8, usize), + pub roc_crashed: extern "C" fn(*mut RocHost, *const u8, usize), +} + +impl RocHost { + /// Allocate memory with the given alignment and length. + /// + /// # Safety + /// The returned pointer must be used only according to Roc allocation layout + /// rules and later released through the matching host deallocator. + #[inline] + pub unsafe fn alloc(&self, alignment: usize, length: usize) -> *mut c_void { + let host = self as *const RocHost as *mut RocHost; + (self.roc_alloc)(host, length, alignment) + } + + /// Deallocate memory previously allocated with `alloc`. + /// + /// # Safety + /// `ptr` must have been allocated by this host with the same alignment and must + /// not be used after this call. + #[inline] + pub unsafe fn dealloc(&self, ptr: *mut c_void, alignment: usize) { + let host = self as *const RocHost as *mut RocHost; + (self.roc_dealloc)(host, ptr, alignment); + } + + /// Reallocate memory to a new size. + /// + /// # Safety + /// `old_ptr` must have been allocated by this host with the same alignment. + /// The returned pointer replaces `old_ptr`; the old pointer must not be used. + #[inline] + pub unsafe fn realloc( + &self, + old_ptr: *mut c_void, + alignment: usize, + new_length: usize, + ) -> *mut c_void { + let host = self as *const RocHost as *mut RocHost; + (self.roc_realloc)(host, old_ptr, new_length, alignment) + } +} + +/// Uniform ABI function pointer stored in `RocErasedCallablePayload`. +pub type RocErasedCallableFn = extern "C" fn(*mut RocHost, *mut u8, *const u8, *mut u8); + +/// Final-drop callback for inline erased-callable captures. +pub type RocErasedCallableOnDrop = extern "C" fn(*mut u8, *mut RocHost); + +/// Payload header for `Box(function)`. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct RocErasedCallablePayload { + pub callable_fn_ptr: RocErasedCallableFn, + pub on_drop: Option, +} + +/// Runtime representation of `Box(function)`. +pub type RocErasedCallable = *mut u8; + +pub const ROC_ERASED_CALLABLE_CAPTURE_ALIGNMENT: usize = 16; +pub const ROC_ERASED_CALLABLE_PAYLOAD_ALIGNMENT: usize = 16; +pub const ROC_ERASED_CALLABLE_CAPTURE_OFFSET: usize = + (core::mem::size_of::() + 15) & !15; + +#[inline] +pub const fn roc_erased_callable_payload_size(capture_size: usize) -> usize { + ROC_ERASED_CALLABLE_CAPTURE_OFFSET + capture_size +} + +#[inline] +/// # Safety +/// `callable` must be a non-null Roc erased-callable data pointer. +pub unsafe fn roc_erased_callable_payload_ptr(callable: RocErasedCallable) -> *mut RocErasedCallablePayload { + callable as *mut RocErasedCallablePayload +} + +#[inline] +/// # Safety +/// `callable` must be a non-null Roc erased-callable data pointer. +pub unsafe fn roc_erased_callable_capture_ptr(callable: RocErasedCallable) -> *mut u8 { + callable.add(ROC_ERASED_CALLABLE_CAPTURE_OFFSET) +} + +/// Allocate a Roc erased callable payload. +/// +/// # Safety +/// The caller must initialize and use the returned callable according to Roc's +/// erased-callable ABI. `callable_fn_ptr` and `on_drop` must have matching ABI +/// signatures for the captured payload. +pub unsafe fn roc_erased_callable_allocate( + roc_host: &RocHost, + callable_fn_ptr: RocErasedCallableFn, + on_drop: Option, + capture_size: usize, +) -> RocErasedCallable { + let ptr_width = core::mem::size_of::(); + let alignment = core::cmp::max(ptr_width, ROC_ERASED_CALLABLE_PAYLOAD_ALIGNMENT); + let extra_bytes = core::cmp::max(ptr_width, ROC_ERASED_CALLABLE_PAYLOAD_ALIGNMENT); + let base = roc_host.alloc(alignment, extra_bytes + roc_erased_callable_payload_size(capture_size)) as *mut u8; + let data = base.add(extra_bytes); + let rc = data.sub(core::mem::size_of::()) as *mut isize; + *rc = 1; + let payload = roc_erased_callable_payload_ptr(data); + *payload = RocErasedCallablePayload { callable_fn_ptr, on_drop }; + data +} + +/// Payload drop callback for a boxed value. +/// +/// The callback receives the boxed payload data pointer and must recursively +/// decref any Roc refcounted values inside the payload. It must not free the +/// box allocation; `decref_box_with` and `free_box_with` free it after the callback. +pub type RocBoxPayloadDecref = extern "C" fn(*mut c_void, *mut RocHost); + +/// Increment the refcount of a boxed payload data pointer. +pub fn incref_box(data_ptr: RocBox, amount: isize) { + let data = match box_data_ptr(data_ptr) { + Some(ptr) => ptr, + None => return, + }; + let rc = box_refcount_ptr(data); + unsafe { + if (*rc).load(Ordering::Relaxed) == 0 { + return; // REFCOUNT_STATIC_DATA + } + (*rc).fetch_add(amount, Ordering::Relaxed); + } +} + +/// Allocate a Roc box and return a pointer to its payload data. +pub fn allocate_box( + payload_size: usize, + payload_alignment: usize, + payload_contains_refcounted: bool, + roc_host: &RocHost, +) -> RocBox { + let ptr_width = core::mem::size_of::(); + let required_space = if payload_contains_refcounted { 2 * ptr_width } else { ptr_width }; + let header_bytes = required_space.max(payload_alignment); + let alloc_alignment = ptr_width.max(payload_alignment); + let base = unsafe { roc_host.alloc(alloc_alignment, header_bytes + payload_size) } as *mut u8; + let data = unsafe { base.add(header_bytes) }; + unsafe { + let rc = data.sub(core::mem::size_of::()) as *mut isize; + *rc = 1; + } + data as RocBox +} + +/// Decrement a pointer-aligned boxed payload with no Roc refcounted values. +pub fn decref_box(data_ptr: RocBox, roc_host: &RocHost) { + decref_box_with(data_ptr, core::mem::align_of::(), false, None, roc_host); +} + +/// Increment a boxed function closure. +pub fn incref_erased_callable(callable: RocErasedCallable, amount: isize) { + incref_box(callable as RocBox, amount); +} + +/// Decrement a boxed function closure and run its capture drop callback on final release. +pub fn decref_erased_callable(callable: RocErasedCallable, roc_host: &RocHost) { + decref_box_with( + callable as RocBox, + ROC_ERASED_CALLABLE_PAYLOAD_ALIGNMENT, + false, + Some(drop_erased_callable_payload), + roc_host, + ); +} + +extern "C" fn drop_erased_callable_payload(data_ptr: *mut c_void, roc_host: *mut RocHost) { + if data_ptr.is_null() || roc_host.is_null() { + return; + } + unsafe { + let callable = data_ptr as RocErasedCallable; + let payload = roc_erased_callable_payload_ptr(callable); + if let Some(on_drop) = (*payload).on_drop { + on_drop(roc_erased_callable_capture_ptr(callable), roc_host); + } + } +} + +/// Decrement a boxed payload and run payload teardown when this is the final ref. +/// +/// `payload_contains_refcounted` must match the value passed to `allocate_box`: +/// it determines the box header size, and is independent of whether a +/// `payload_decref` teardown callback is supplied. A host resource handle such +/// as `Box(U64)` holding a raw pointer has `payload_contains_refcounted: false` +/// even when it provides a teardown callback to free the underlying resource. +pub fn decref_box_with( + data_ptr: RocBox, + payload_alignment: usize, + payload_contains_refcounted: bool, + payload_decref: Option, + roc_host: &RocHost, +) { + let data = match box_data_ptr(data_ptr) { + Some(ptr) => ptr, + None => return, + }; + let rc = box_refcount_ptr(data); + unsafe { + if (*rc).load(Ordering::Relaxed) == 0 { + return; // REFCOUNT_STATIC_DATA + } + let prev = (*rc).fetch_sub(1, Ordering::Relaxed); + if prev == 1 { + if let Some(callback) = payload_decref { + callback(data_ptr, roc_host as *const RocHost as *mut RocHost); + } + free_box_allocation(data, payload_alignment, payload_contains_refcounted, roc_host); + } + } +} + +/// Free a boxed payload allocation immediately after running payload teardown. +/// +/// See `decref_box_with` for the meaning of `payload_contains_refcounted`. +pub fn free_box_with( + data_ptr: RocBox, + payload_alignment: usize, + payload_contains_refcounted: bool, + payload_decref: Option, + roc_host: &RocHost, +) { + let data = match box_data_ptr(data_ptr) { + Some(ptr) => ptr, + None => return, + }; + if let Some(callback) = payload_decref { + callback(data_ptr, roc_host as *const RocHost as *mut RocHost); + } + free_box_allocation(data, payload_alignment, payload_contains_refcounted, roc_host); +} + +/// Return true when a boxed payload data pointer has exactly one live ref. +pub fn is_unique_box(data_ptr: RocBox) -> bool { + let data = match box_data_ptr(data_ptr) { + Some(ptr) => ptr, + None => return true, + }; + let rc = box_refcount_ptr(data); + unsafe { (*rc).load(Ordering::Relaxed) == 1 } +} + +fn box_data_ptr(data_ptr: RocBox) -> Option<*mut u8> { + if data_ptr.is_null() { + None + } else { + Some(data_ptr as *mut u8) + } +} + +fn box_refcount_ptr(data: *mut u8) -> *mut AtomicIsize { + unsafe { data.sub(core::mem::size_of::()) as *mut AtomicIsize } +} + +fn free_box_allocation( + data: *mut u8, + payload_alignment: usize, + payload_contains_refcounted: bool, + roc_host: &RocHost, +) { + let ptr_width = core::mem::size_of::(); + let required_space = if payload_contains_refcounted { 2 * ptr_width } else { ptr_width }; + let header_bytes = required_space.max(payload_alignment); + let alloc_alignment = ptr_width.max(payload_alignment); + let base = unsafe { data.sub(header_bytes) } as *mut c_void; + unsafe { + roc_host.dealloc(base, alloc_alignment); + } +} + +/// A Roc string value. Small strings (up to 23 bytes on 64-bit) are stored inline; +/// larger strings are heap-allocated with a reference count. +/// +/// `bytes` is never tagged. Operations, host code, glue code, and object-file +/// relocations can use it directly as the UTF-8 byte pointer for non-small +/// strings. Seamless-slice tagging lives in `capacity_or_alloc_ptr` instead. +/// Big-string capacity is stored shifted left by one bit, so max capacity is +/// essentially `isize::MAX` bytes: about 2 GiB on 32-bit targets and 8 EiB on +/// 64-bit targets. +/// +/// This type is ABI-compatible with the Zig RocStr (24 bytes, `#[repr(C)]`). +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RocStr { + pub bytes: *mut u8, + pub capacity_or_alloc_ptr: usize, + pub length: usize, +} + +const ROC_STR_SIZE: usize = core::mem::size_of::(); +const ROC_SMALL_STR_MAX_LEN: usize = ROC_STR_SIZE - 1; +const ROC_SMALL_STR_BIT: usize = isize::MIN as usize; +const ROC_SEAMLESS_SLICE_TAG: usize = 1; + +impl RocStr { + /// Return an empty RocStr (small string with zero length). + pub fn empty() -> Self { + Self { + bytes: core::ptr::null_mut(), + capacity_or_alloc_ptr: 0, + length: ROC_SMALL_STR_BIT, + } + } + + /// Return true if this string is stored inline (small string optimization). + #[inline] + pub fn is_small_str(&self) -> bool { + (self.length as isize) < 0 + } + + /// Return true if this string is a seamless slice into another allocation. + #[inline] + pub fn is_seamless_slice(&self) -> bool { + !self.is_small_str() && (self.capacity_or_alloc_ptr & ROC_SEAMLESS_SLICE_TAG) != 0 + } + + /// Return the length of the string in bytes. + #[inline] + pub fn len(&self) -> usize { + if self.is_small_str() { + let bytes_ptr = self as *const Self as *const u8; + let last_byte = unsafe { *bytes_ptr.add(ROC_STR_SIZE - 1) }; + (last_byte ^ 0b1000_0000) as usize + } else { + self.length + } + } + + /// Return true if the string has zero length. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Return the string contents as a byte slice. + pub fn as_slice(&self) -> &[u8] { + let ptr = self.as_u8_ptr(); + unsafe { core::slice::from_raw_parts(ptr, self.len()) } + } + + /// Return a pointer to the raw UTF-8 bytes. + #[inline] + pub fn as_u8_ptr(&self) -> *const u8 { + if self.is_small_str() { + self as *const Self as *const u8 + } else { + self.bytes as *const u8 + } + } + + /// Return the string contents as a `&str`, assuming valid UTF-8. + pub fn as_str(&self) -> &str { + // SAFETY: Roc guarantees all strings are valid UTF-8. + unsafe { core::str::from_utf8_unchecked(self.as_slice()) } + } + + /// Create a RocStr from a byte slice, using `roc_host` for heap allocation if needed. + pub fn from_slice(slice: &[u8], roc_host: &RocHost) -> Self { + if slice.len() < ROC_STR_SIZE { + let mut result = Self::empty(); + let ptr = &mut result as *mut Self as *mut u8; + unsafe { + core::ptr::copy_nonoverlapping(slice.as_ptr(), ptr, slice.len()); + *ptr.add(ROC_STR_SIZE - 1) = (slice.len() as u8) | 0b1000_0000; + } + result + } else { + let ptr_width = core::mem::size_of::(); + let total = ptr_width + slice.len(); + let base = unsafe { roc_host.alloc(core::mem::align_of::(), total) }; + let data_ptr = unsafe { (base as *mut u8).add(ptr_width) }; + // Write refcount = 1 + unsafe { + let rc = (data_ptr as *mut isize).sub(1); + *rc = 1; + core::ptr::copy_nonoverlapping(slice.as_ptr(), data_ptr, slice.len()); + } + Self { + bytes: data_ptr, + capacity_or_alloc_ptr: slice.len() << 1, + length: slice.len(), + } + } + } + + /// Create a RocStr from a `&str`. + pub fn from_str(s: &str, roc_host: &RocHost) -> Self { + Self::from_slice(s.as_bytes(), roc_host) + } + + /// Decrement the reference count; frees the allocation when it reaches zero. + pub fn decref(&self, roc_host: &RocHost) { + if self.is_small_str() { + return; + } + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return; + } + unsafe { + let rc = (alloc_ptr as *mut AtomicIsize).sub(1); + if (*rc).load(Ordering::Relaxed) == 0 { + return; // REFCOUNT_STATIC_DATA — bytes are in read-only memory + } + let prev = (*rc).fetch_sub(1, Ordering::Relaxed); + if prev == 1 { + let ptr_width = core::mem::size_of::(); + let base = alloc_ptr.sub(ptr_width) as *mut c_void; + roc_host.dealloc(base, core::mem::align_of::()); + } + } + } + + /// Increment the reference count by `amount`. + pub fn incref(&self, amount: isize) { + if self.is_small_str() { + return; + } + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return; + } + unsafe { + let rc = (alloc_ptr as *mut AtomicIsize).sub(1); + if (*rc).load(Ordering::Relaxed) == 0 { + return; // REFCOUNT_STATIC_DATA + } + (*rc).fetch_add(amount, Ordering::Relaxed); + } + } + + /// Return true if this string has a reference count of exactly one. + pub fn is_unique(&self) -> bool { + if self.is_small_str() { + return true; + } + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return true; + } + unsafe { + let rc = (alloc_ptr as *const AtomicIsize).sub(1); + let count = (*rc).load(Ordering::Relaxed); + count == 0 || count == 1 + } + } + + fn get_allocation_ptr(&self) -> *mut u8 { + if self.is_seamless_slice() { + (self.capacity_or_alloc_ptr & !ROC_SEAMLESS_SLICE_TAG) as *mut u8 + } else { + self.bytes + } + } +} + +impl core::fmt::Debug for RocStr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("RocStr") + .field("value", &self.as_str()) + .field("len", &self.len()) + .field("is_small", &self.is_small_str()) + .finish() + } +} + +/// A generic Roc list. Elements are reference-counted and heap-allocated. +/// +/// When `ELEMENTS_REFCOUNTED` is true (the default via `RocList`), an extra +/// `ptr_width` bytes are reserved in the allocation header for the element count, +/// matching the Roc runtime's `allocateWithRefcount` layout. +pub type RocList = RocListWith; + +/// Parameterized list constructor; use `RocList` for refcounted elements. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RocListWith { + pub elements: *mut T, + pub length: usize, + pub capacity_or_alloc_ptr: usize, +} + +impl RocListWith { + #[inline] + fn header_bytes() -> usize { + let ptr_width = core::mem::size_of::(); + let required_space = if ELEMENTS_REFCOUNTED { 2 * ptr_width } else { ptr_width }; + required_space.max(core::mem::align_of::()) + } + + /// Return an empty RocList. + pub fn empty() -> Self { + Self { + elements: core::ptr::null_mut(), + length: 0, + capacity_or_alloc_ptr: 0, + } + } + + /// Return the number of elements in the list. + #[inline] + pub fn len(&self) -> usize { + self.length + } + + /// Return true if the list has zero elements. + #[inline] + pub fn is_empty(&self) -> bool { + self.length == 0 + } + + /// Return true if this list is a seamless slice into another allocation. + /// Slices share the rc slot with their backing allocation; the alloc ptr is + /// encoded in `capacity_or_alloc_ptr` with the low bit set. + #[inline] + pub fn is_seamless_slice(&self) -> bool { + (self.capacity_or_alloc_ptr & 1) != 0 + } + + /// Resolve `self` to the start of its backing allocation (the element block + /// just after the rc slot). Returns `null` for empty lists. Handles both + /// whole-backing and seamless-slice forms. + fn get_allocation_ptr(&self) -> *mut u8 { + if self.is_seamless_slice() { + (self.capacity_or_alloc_ptr & !1) as *mut u8 + } else { + self.elements as *mut u8 + } + } + + fn allocation_element_count(&self) -> usize { + if self.is_seamless_slice() && ELEMENTS_REFCOUNTED { + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return 0; + } + unsafe { + let ptr = alloc_ptr as *const usize; + *ptr.sub(2) + } + } else { + self.length + } + } + + /// Return the list elements as a slice. + pub fn as_slice(&self) -> &[T] { + if self.elements.is_null() { + &[] + } else { + unsafe { core::slice::from_raw_parts(self.elements, self.length) } + } + } + + /// Return all items in the backing allocation, not just this slice. + pub fn allocation_items(&self) -> &[T] { + if self.elements.is_null() { + &[] + } else { + unsafe { core::slice::from_raw_parts(self.get_allocation_ptr() as *const T, self.allocation_element_count()) } + } + } + + /// Allocate a new list with space for `length` elements. + pub fn allocate(length: usize, roc_host: &RocHost) -> Self { + if length == 0 { + return Self::empty(); + } + let align = core::mem::align_of::().max(core::mem::align_of::()); + let header_bytes = Self::header_bytes(); + let data_bytes = length * core::mem::size_of::(); + let total = data_bytes + header_bytes; + let base = unsafe { roc_host.alloc(align, total) }; + let data_ptr = unsafe { (base as *mut u8).add(header_bytes) }; + // Write refcount = 1 + unsafe { + let rc = (data_ptr as *mut isize).sub(1); + *rc = 1; + } + Self { + elements: data_ptr as *mut T, + length, + capacity_or_alloc_ptr: length << 1, + } + } + + /// Create a RocList from a slice, copying elements into a new allocation. + pub fn from_slice(slice: &[T], roc_host: &RocHost) -> Self where T: Copy { + if slice.is_empty() { + return Self::empty(); + } + let list = Self::allocate(slice.len(), roc_host); + unsafe { + core::ptr::copy_nonoverlapping( + slice.as_ptr(), + list.elements, + slice.len(), + ); + } + list + } + + /// Decrement the reference count; frees the allocation when it reaches zero. + pub fn decref(&self, roc_host: &RocHost) { + if self.elements.is_null() { + return; + } + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return; + } + let align = core::mem::align_of::().max(core::mem::align_of::()); + let header_bytes = Self::header_bytes(); + unsafe { + let rc = (alloc_ptr as *mut AtomicIsize).sub(1); + if (*rc).load(Ordering::Relaxed) == 0 { + return; // REFCOUNT_STATIC_DATA — elements are in read-only memory + } + let prev = (*rc).fetch_sub(1, Ordering::Relaxed); + if prev == 1 { + let base = alloc_ptr.sub(header_bytes) as *mut c_void; + roc_host.dealloc(base, align); + } + } + } + + /// Increment the reference count by `amount`. + pub fn incref(&self, amount: isize) { + if self.elements.is_null() { + return; + } + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return; + } + unsafe { + let rc = (alloc_ptr as *mut AtomicIsize).sub(1); + if (*rc).load(Ordering::Relaxed) == 0 { + return; // REFCOUNT_STATIC_DATA + } + (*rc).fetch_add(amount, Ordering::Relaxed); + } + } + + /// Return true if this list has a reference count of exactly one. + pub fn is_unique(&self) -> bool { + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return true; + } + unsafe { + let rc = (alloc_ptr as *const AtomicIsize).sub(1); + let count = (*rc).load(Ordering::Relaxed); + count == 0 || count == 1 + } + } + + /// Return true if this list's allocation has exactly one counted ref. + pub fn has_one_ref(&self) -> bool { + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return false; + } + unsafe { + let rc = (alloc_ptr as *const AtomicIsize).sub(1); + (*rc).load(Ordering::Relaxed) == 1 + } + } +} + +impl core::fmt::Debug for RocListWith { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries(self.as_slice().iter()).finish() + } +} + +/// Element type for Cmd +#[repr(C)] +#[derive(Clone, Copy)] +pub struct Cmd { + pub args: RocList, + pub envs: RocList, + pub program: RocStr, + pub clear_envs: bool, +} + +const _: () = assert!(core::mem::size_of::() == 80, "Cmd size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "Cmd alignment mismatch"); + +/// Element type for __AnonStruct9 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct9 { + pub stderr_bytes: RocListWith, + pub stdout_bytes: RocListWith, + pub exit_code: i32, +} + +const _: () = assert!(core::mem::size_of::() == 56, "AnonStruct9 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct9 alignment mismatch"); + +/// Element type for __AnonStruct12 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct12 { + pub stderr_bytes: RocListWith, + pub stdout_bytes: RocListWith, +} + +const _: () = assert!(core::mem::size_of::() == 48, "AnonStruct12 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct12 alignment mismatch"); + +/// Element type for __AnonStruct53 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct53 { + pub body: RocListWith, + pub headers: RocList, + pub status: u16, +} + +const _: () = assert!(core::mem::size_of::() == 56, "AnonStruct53 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct53 alignment mismatch"); + +/// Element type for __AnonStruct57 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct57 { + pub name: RocStr, + pub value: RocStr, +} + +const _: () = assert!(core::mem::size_of::() == 48, "AnonStruct57 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct57 alignment mismatch"); + +/// Element type for __AnonStruct60 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct60 { + pub body: RocListWith, + pub headers: RocList, + pub method: u64, + pub method_ext: RocStr, + pub timeout_ms: u64, + pub uri: RocStr, +} + +const _: () = assert!(core::mem::size_of::() == 112, "AnonStruct60 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct60 alignment mismatch"); + +/// Element type for __AnonStruct69 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct69 { + pub is_dir: bool, + pub is_file: bool, + pub is_sym_link: bool, +} + +const _: () = assert!(core::mem::size_of::() == 3, "AnonStruct69 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "AnonStruct69 alignment mismatch"); + +/// Element type for __AnonStruct85 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct85 { + pub code: i64, + pub message: RocStr, +} + +const _: () = assert!(core::mem::size_of::() == 32, "AnonStruct85 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct85 alignment mismatch"); + +/// Element type for __AnonStruct93 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct93 { + pub name: RocStr, + pub value: BytesOrIntegerOrNullOrRealOrString, +} + +const _: () = assert!(core::mem::size_of::() == 56, "AnonStruct93 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct93 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType0Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType0 { + pub payload: TryType0Payload, + pub tag: TryType0Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType0Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType0 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType0 alignment mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrType1Tag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + NotFound = 3, + Other = 4, + OutOfMemory = 5, + PermissionDenied = 6, + Unsupported = 7, +} + +/// Tag union: IOErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErrType1 { + pub payload: IOErrType1Payload, + pub tag: IOErrType1Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrType1Payload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub not_found: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub out_of_memory: [u8; 0], + pub permission_denied: [u8; 0], + pub unsupported: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "IOErrType1 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErrType1 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType7Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType7 { + pub payload: TryType7Payload, + pub tag: TryType7Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType7Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 72, "TryType7 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType7 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType8Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType8 { + pub payload: TryType8Payload, + pub tag: TryType8Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType8Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 64, "TryType8 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType8 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType13Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType13 { + pub payload: TryType13Payload, + pub tag: TryType13Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType13Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType13 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType13 alignment mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrType15Tag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + NotFound = 3, + Other = 4, + OutOfMemory = 5, + PermissionDenied = 6, + Unsupported = 7, +} + +/// Tag union: IOErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErrType15 { + pub payload: IOErrType15Payload, + pub tag: IOErrType15Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrType15Payload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub not_found: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub out_of_memory: [u8; 0], + pub permission_denied: [u8; 0], + pub unsupported: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "IOErrType15 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErrType15 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType18Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType18 { + pub payload: TryType18Payload, + pub tag: TryType18Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType18Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType18 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType18 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType21Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType21 { + pub payload: TryType21Payload, + pub tag: TryType21Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType21Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType21 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType21 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType24Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType24 { + pub payload: TryType24Payload, + pub tag: TryType24Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType24Payload { + pub err: core::mem::ManuallyDrop<*mut c_void>, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType24 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType24 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType27Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType27 { + pub payload: TryType27Payload, + pub tag: TryType27Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType27Payload { + pub err: core::mem::ManuallyDrop<*mut c_void>, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType27 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType27 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType29Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType29 { + pub payload: TryType29Payload, + pub tag: TryType29Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType29Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType29 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType29 alignment mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrType31Tag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + NotFound = 3, + Other = 4, + OutOfMemory = 5, + PermissionDenied = 6, + Unsupported = 7, +} + +/// Tag union: IOErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErrType31 { + pub payload: IOErrType31Payload, + pub tag: IOErrType31Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrType31Payload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub not_found: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub out_of_memory: [u8; 0], + pub permission_denied: [u8; 0], + pub unsupported: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "IOErrType31 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErrType31 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType35Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType35 { + pub payload: TryType35Payload, + pub tag: TryType35Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType35Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType35 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType35 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType38Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType38 { + pub payload: TryType38Payload, + pub tag: TryType38Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType38Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType38 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType38 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType40Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType40 { + pub payload: TryType40Payload, + pub tag: TryType40Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType40Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType40 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType40 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType42Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType42 { + pub payload: TryType42Payload, + pub tag: TryType42Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType42Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType42 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType42 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType44Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType44 { + pub payload: TryType44Payload, + pub tag: TryType44Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType44Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType44 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType44 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType47Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType47 { + pub payload: TryType47Payload, + pub tag: TryType47Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType47Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType47 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType47 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType50Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType50 { + pub payload: TryType50Payload, + pub tag: TryType50Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType50Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 48, "TryType50 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 16, "TryType50 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType62Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType62 { + pub payload: TryType62Payload, + pub tag: TryType62Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType62Payload { + pub err: core::mem::ManuallyDrop<*mut c_void>, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType62 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType62 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType66Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType66 { + pub payload: TryType66Payload, + pub tag: TryType66Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType66Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType66 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType66 alignment mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrType67Tag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + NotFound = 3, + Other = 4, + OutOfMemory = 5, + PermissionDenied = 6, + Unsupported = 7, +} + +/// Tag union: IOErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErrType67 { + pub payload: IOErrType67Payload, + pub tag: IOErrType67Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrType67Payload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub not_found: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub out_of_memory: [u8; 0], + pub permission_denied: [u8; 0], + pub unsupported: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "IOErrType67 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErrType67 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType73Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType73 { + pub payload: TryType73Payload, + pub tag: TryType73Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType73Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType73 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType73 alignment mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrType75Tag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + NotFound = 3, + Other = 4, + OutOfMemory = 5, + PermissionDenied = 6, + Unsupported = 7, +} + +/// Tag union: IOErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErrType75 { + pub payload: IOErrType75Payload, + pub tag: IOErrType75Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrType75Payload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub not_found: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub out_of_memory: [u8; 0], + pub permission_denied: [u8; 0], + pub unsupported: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "IOErrType75 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErrType75 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType79Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType79 { + pub payload: TryType79Payload, + pub tag: TryType79Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType79Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType79 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType79 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType84Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType84 { + pub payload: TryType84Payload, + pub tag: TryType84Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType84Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<*mut u64>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType84 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType84 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType90Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType90 { + pub payload: TryType90Payload, + pub tag: TryType90Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType90Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType90 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType90 alignment mismatch"); + +/// Tag discriminant for BytesOrIntegerOrNullOrRealOrString. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BytesOrIntegerOrNullOrRealOrStringTag { + Bytes = 0, + Integer = 1, + Null = 2, + Real = 3, + String = 4, +} + +/// Tag union: BytesOrIntegerOrNullOrRealOrString +#[repr(C)] +#[derive(Clone, Copy)] +pub struct BytesOrIntegerOrNullOrRealOrString { + pub payload: BytesOrIntegerOrNullOrRealOrStringPayload, + pub tag: BytesOrIntegerOrNullOrRealOrStringTag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union BytesOrIntegerOrNullOrRealOrStringPayload { + pub bytes: core::mem::ManuallyDrop>, + pub integer: core::mem::ManuallyDrop, + pub null: [u8; 0], + pub real: core::mem::ManuallyDrop, + pub string: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 32, "BytesOrIntegerOrNullOrRealOrString size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "BytesOrIntegerOrNullOrRealOrString alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType99Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType99 { + pub payload: TryType99Payload, + pub tag: TryType99Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType99Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType99 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType99 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType100Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType100 { + pub payload: TryType100Payload, + pub tag: TryType100Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType100Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType100 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType100 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType102Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType102 { + pub payload: TryType102Payload, + pub tag: TryType102Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType102Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType102 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType102 alignment mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrType104Tag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + NotFound = 3, + Other = 4, + OutOfMemory = 5, + PermissionDenied = 6, + Unsupported = 7, +} + +/// Tag union: IOErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErrType104 { + pub payload: IOErrType104Payload, + pub tag: IOErrType104Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrType104Payload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub not_found: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub out_of_memory: [u8; 0], + pub permission_denied: [u8; 0], + pub unsupported: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "IOErrType104 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErrType104 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType107Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType107 { + pub payload: TryType107Payload, + pub tag: TryType107Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType107Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType107 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType107 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType111Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType111 { + pub payload: TryType111Payload, + pub tag: TryType111Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType111Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 48, "TryType111 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType111 alignment mismatch"); + +/// Tag discriminant for EndOfFileOrStdinErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EndOfFileOrStdinErrType112Tag { + EndOfFile = 0, + StdinErr = 1, +} + +/// Tag union: EndOfFileOrStdinErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EndOfFileOrStdinErrType112 { + pub payload: EndOfFileOrStdinErrType112Payload, + pub tag: EndOfFileOrStdinErrType112Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union EndOfFileOrStdinErrType112Payload { + pub end_of_file: [u8; 0], + pub stdin_err: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "EndOfFileOrStdinErrType112 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "EndOfFileOrStdinErrType112 alignment mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrType113Tag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + NotFound = 3, + Other = 4, + OutOfMemory = 5, + PermissionDenied = 6, + Unsupported = 7, +} + +/// Tag union: IOErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErrType113 { + pub payload: IOErrType113Payload, + pub tag: IOErrType113Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrType113Payload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub not_found: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub out_of_memory: [u8; 0], + pub permission_denied: [u8; 0], + pub unsupported: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "IOErrType113 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErrType113 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType116Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType116 { + pub payload: TryType116Payload, + pub tag: TryType116Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType116Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 48, "TryType116 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType116 alignment mismatch"); + +/// Tag discriminant for EndOfFileOrStdinErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EndOfFileOrStdinErrType117Tag { + EndOfFile = 0, + StdinErr = 1, +} + +/// Tag union: EndOfFileOrStdinErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EndOfFileOrStdinErrType117 { + pub payload: EndOfFileOrStdinErrType117Payload, + pub tag: EndOfFileOrStdinErrType117Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union EndOfFileOrStdinErrType117Payload { + pub end_of_file: [u8; 0], + pub stdin_err: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "EndOfFileOrStdinErrType117 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "EndOfFileOrStdinErrType117 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType120Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType120 { + pub payload: TryType120Payload, + pub tag: TryType120Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType120Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType120 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType120 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType122Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType122 { + pub payload: TryType122Payload, + pub tag: TryType122Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType122Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType122 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType122 alignment mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrType124Tag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + NotFound = 3, + Other = 4, + OutOfMemory = 5, + PermissionDenied = 6, + Unsupported = 7, +} + +/// Tag union: IOErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErrType124 { + pub payload: IOErrType124Payload, + pub tag: IOErrType124Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrType124Payload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub not_found: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub out_of_memory: [u8; 0], + pub permission_denied: [u8; 0], + pub unsupported: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "IOErrType124 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErrType124 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType127Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType127 { + pub payload: TryType127Payload, + pub tag: TryType127Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType127Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType127 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType127 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType131Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType131 { + pub payload: TryType131Payload, + pub tag: TryType131Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType131Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<*mut u64>, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType131 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType131 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType136Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType136 { + pub payload: TryType136Payload, + pub tag: TryType136Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType136Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType136 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType136 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType139Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType139 { + pub payload: TryType139Payload, + pub tag: TryType139Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType139Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType139 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType139 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType147Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType147 { + pub payload: TryType147Payload, + pub tag: TryType147Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType147Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 8, "TryType147 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 4, "TryType147 alignment mismatch"); + +/// Return type record for Http.host_send_request! +/// Fields ordered by alignment descending (Roc ABI) +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HttpHostSendRequestRetRecord { + pub body: RocListWith, + pub headers: RocList, + pub status: u16, +} + +const _: () = assert!(core::mem::size_of::() == 56, "HttpHostSendRequestRetRecord size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "HttpHostSendRequestRetRecord alignment mismatch"); + +/// Arguments for Cmd.host_exec_exit_code! +/// Roc signature: Cmd => Try(I32, IOErr) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct CmdHostExecExitCodeArgs { + pub args: RocList, + pub envs: RocList, + pub program: RocStr, + pub clear_envs: bool, +} + +const _: () = assert!(core::mem::size_of::() == 80, "CmdHostExecExitCodeArgs size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "CmdHostExecExitCodeArgs alignment mismatch"); + +/// Arguments for Cmd.host_exec_output! +/// Roc signature: Cmd => Try({ stderr_bytes : List(U8), stdout_bytes : List(U8) }, Try({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) }, IOErr)) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct CmdHostExecOutputArgs { + pub args: RocList, + pub envs: RocList, + pub program: RocStr, + pub clear_envs: bool, +} + +const _: () = assert!(core::mem::size_of::() == 80, "CmdHostExecOutputArgs size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "CmdHostExecOutputArgs alignment mismatch"); + +/// Arguments for Dir.create! +/// Roc signature: Str => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DirCreateArgs { + pub arg0: RocStr, +} + +/// Arguments for Dir.create_all! +/// Roc signature: Str => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DirCreateAllArgs { + pub arg0: RocStr, +} + +/// Arguments for Dir.delete_all! +/// Roc signature: Str => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DirDeleteAllArgs { + pub arg0: RocStr, +} + +/// Arguments for Dir.delete_empty! +/// Roc signature: Str => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DirDeleteEmptyArgs { + pub arg0: RocStr, +} + +/// Arguments for Dir.list! +/// Roc signature: Str => Try(List(Str), [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DirListArgs { + pub arg0: RocStr, +} + +/// Arguments for Env.var! +/// Roc signature: Str => Try(Str, [VarNotFound(Str)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EnvVarArgs { + pub arg0: RocStr, +} + +/// Arguments for File.delete! +/// Roc signature: Str => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileDeleteArgs { + pub arg0: RocStr, +} + +/// Arguments for File.is_executable! +/// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileIsExecutableArgs { + pub arg0: RocStr, +} + +/// Arguments for File.is_readable! +/// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileIsReadableArgs { + pub arg0: RocStr, +} + +/// Arguments for File.is_writable! +/// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileIsWritableArgs { + pub arg0: RocStr, +} + +/// Arguments for File.read_bytes! +/// Roc signature: Str => Try(List(U8), [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileReadBytesArgs { + pub arg0: RocStr, +} + +/// Arguments for File.read_utf8! +/// Roc signature: Str => Try(Str, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileReadUtf8Args { + pub arg0: RocStr, +} + +/// Arguments for File.size_in_bytes! +/// Roc signature: Str => Try(U64, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileSizeInBytesArgs { + pub arg0: RocStr, +} + +/// Arguments for File.time_accessed! +/// Roc signature: Str => Try(U128, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileTimeAccessedArgs { + pub arg0: RocStr, +} + +/// Arguments for File.time_created! +/// Roc signature: Str => Try(U128, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileTimeCreatedArgs { + pub arg0: RocStr, +} + +/// Arguments for File.time_modified! +/// Roc signature: Str => Try(U128, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileTimeModifiedArgs { + pub arg0: RocStr, +} + +/// Arguments for File.write_bytes! +/// Roc signature: Str, List(U8) => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileWriteBytesArgs { + pub arg0: RocStr, + pub arg1: RocListWith, +} + +/// Arguments for File.write_utf8! +/// Roc signature: Str, Str => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FileWriteUtf8Args { + pub arg0: RocStr, + pub arg1: RocStr, +} + +/// Arguments for Http.host_send_request! +/// Roc signature: { body : List(U8), headers : List({ name : Str, value : Str }), method : U64, method_ext : Str, timeout_ms : U64, uri : Str } => { body : List(U8), headers : List({ name : Str, value : Str }), status : U16 } +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HttpHostSendRequestArgs { + pub body: RocListWith, + pub headers: RocList, + pub method: u64, + pub method_ext: RocStr, + pub timeout_ms: u64, + pub uri: RocStr, +} + +const _: () = assert!(core::mem::size_of::() == 112, "HttpHostSendRequestArgs size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "HttpHostSendRequestArgs alignment mismatch"); + +/// Arguments for Path.host_path_type! +/// Roc signature: List(U8) => Try({ is_dir : Bool, is_file : Bool, is_sym_link : Bool }, IOErr) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct PathHostPathTypeArgs { + pub arg0: RocListWith, +} + +/// Arguments for Sleep.millis! +/// Roc signature: U64 => {} +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct SleepMillisArgs { + pub arg0: u64, +} + +/// Arguments for Sqlite.host_bind! +/// Roc signature: Sqlite.Stmt, List({ name : Str, value : [Bytes(List(U8)), Integer(I64), Null, Real(F64), String(Str)] }) => Try({}, { code : I64, message : Str }) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct SqliteHostBindArgs { + pub arg0: *mut u64, + pub arg1: RocList, +} + +/// Arguments for Sqlite.host_column_value! +/// Roc signature: Sqlite.Stmt, U64 => Try([Bytes(List(U8)), Integer(I64), Null, Real(F64), String(Str)], { code : I64, message : Str }) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct SqliteHostColumnValueArgs { + pub arg0: *mut u64, + pub arg1: u64, +} + +/// Arguments for Sqlite.host_columns! +/// Roc signature: Sqlite.Stmt => List(Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct SqliteHostColumnsArgs { + pub arg0: *mut u64, +} + +/// Arguments for Sqlite.host_prepare! +/// Roc signature: Str, Str => Try(Sqlite.Stmt, { code : I64, message : Str }) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct SqliteHostPrepareArgs { + pub arg0: RocStr, + pub arg1: RocStr, +} + +/// Arguments for Sqlite.host_reset! +/// Roc signature: Sqlite.Stmt => Try({}, { code : I64, message : Str }) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct SqliteHostResetArgs { + pub arg0: *mut u64, +} + +/// Arguments for Sqlite.host_step! +/// Roc signature: Sqlite.Stmt => Try(Bool, { code : I64, message : Str }) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct SqliteHostStepArgs { + pub arg0: *mut u64, +} + +/// Arguments for Stderr.line! +/// Roc signature: Str => Try({}, [StderrErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StderrLineArgs { + pub arg0: RocStr, +} + +/// Arguments for Stderr.write! +/// Roc signature: Str => Try({}, [StderrErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StderrWriteArgs { + pub arg0: RocStr, +} + +/// Arguments for Stderr.write_bytes! +/// Roc signature: List(U8) => Try({}, [StderrErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StderrWriteBytesArgs { + pub arg0: RocListWith, +} + +/// Arguments for Stdout.line! +/// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StdoutLineArgs { + pub arg0: RocStr, +} + +/// Arguments for Stdout.write! +/// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StdoutWriteArgs { + pub arg0: RocStr, +} + +/// Arguments for Stdout.write_bytes! +/// Roc signature: List(U8) => Try({}, [StdoutErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StdoutWriteBytesArgs { + pub arg0: RocListWith, +} + +/// Arguments for Tcp.host_connect! +/// Roc signature: Str, U16 => Try(Tcp.Stream, Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TcpHostConnectArgs { + pub arg0: RocStr, + pub arg1: u16, +} + +/// Arguments for Tcp.host_read_exactly! +/// Roc signature: Tcp.Stream, U64 => Try(List(U8), Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TcpHostReadExactlyArgs { + pub arg0: *mut u64, + pub arg1: u64, +} + +/// Arguments for Tcp.host_read_until! +/// Roc signature: Tcp.Stream, U8 => Try(List(U8), Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TcpHostReadUntilArgs { + pub arg0: *mut u64, + pub arg1: u8, +} + +/// Arguments for Tcp.host_read_up_to! +/// Roc signature: Tcp.Stream, U64 => Try(List(U8), Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TcpHostReadUpToArgs { + pub arg0: *mut u64, + pub arg1: u64, +} + +/// Arguments for Tcp.host_write! +/// Roc signature: Tcp.Stream, List(U8) => Try({}, Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TcpHostWriteArgs { + pub arg0: *mut u64, + pub arg1: RocListWith, +} + +// ============================================================================= +// Semantic Type Aliases +// ============================================================================= + +pub type CmdHostExecExitCodeResult = TryType0; +pub type CmdHostExecExitCodeResultPayload = TryType0Payload; +pub type CmdHostExecExitCodeResultTag = TryType0Tag; +pub type CmdIOErr = IOErrType1; +pub type CmdIOErrPayload = IOErrType1Payload; +pub type CmdIOErrTag = IOErrType1Tag; +pub type CmdHostExecOutputResult = TryType7; +pub type CmdHostExecOutputResultPayload = TryType7Payload; +pub type CmdHostExecOutputResultTag = TryType7Tag; +pub type CmdHostExecOutputErrResult = TryType8; +pub type CmdHostExecOutputErrResultPayload = TryType8Payload; +pub type CmdHostExecOutputErrResultTag = TryType8Tag; +pub type CmdHostExecOutputErrOk = AnonStruct9; +pub type CmdHostExecOutputOk = AnonStruct12; +pub type DirCreateResult = TryType13; +pub type DirCreateResultPayload = TryType13Payload; +pub type DirCreateResultTag = TryType13Tag; +pub type DirIOErr = IOErrType15; +pub type DirIOErrPayload = IOErrType15Payload; +pub type DirIOErrTag = IOErrType15Tag; +pub type DirCreateAllResult = TryType13; +pub type DirCreateAllResultPayload = TryType13Payload; +pub type DirCreateAllResultTag = TryType13Tag; +pub type DirDeleteAllResult = TryType13; +pub type DirDeleteAllResultPayload = TryType13Payload; +pub type DirDeleteAllResultTag = TryType13Tag; +pub type DirDeleteEmptyResult = TryType13; +pub type DirDeleteEmptyResultPayload = TryType13Payload; +pub type DirDeleteEmptyResultTag = TryType13Tag; +pub type DirListResult = TryType18; +pub type DirListResultPayload = TryType18Payload; +pub type DirListResultTag = TryType18Tag; +pub type EnvCwdResult = TryType24; +pub type EnvCwdResultPayload = TryType24Payload; +pub type EnvCwdResultTag = TryType24Tag; +pub type EnvExePathResult = TryType27; +pub type EnvExePathResultPayload = TryType27Payload; +pub type EnvExePathResultTag = TryType27Tag; +pub type EnvVarResult = TryType21; +pub type EnvVarResultPayload = TryType21Payload; +pub type EnvVarResultTag = TryType21Tag; +pub type FileDeleteResult = TryType42; +pub type FileDeleteResultPayload = TryType42Payload; +pub type FileDeleteResultTag = TryType42Tag; +pub type FileIOErr = IOErrType31; +pub type FileIOErrPayload = IOErrType31Payload; +pub type FileIOErrTag = IOErrType31Tag; +pub type FileIsExecutableResult = TryType47; +pub type FileIsExecutableResultPayload = TryType47Payload; +pub type FileIsExecutableResultTag = TryType47Tag; +pub type FileIsReadableResult = TryType47; +pub type FileIsReadableResultPayload = TryType47Payload; +pub type FileIsReadableResultTag = TryType47Tag; +pub type FileIsWritableResult = TryType47; +pub type FileIsWritableResultPayload = TryType47Payload; +pub type FileIsWritableResultTag = TryType47Tag; +pub type FileReadBytesResult = TryType29; +pub type FileReadBytesResultPayload = TryType29Payload; +pub type FileReadBytesResultTag = TryType29Tag; +pub type FileReadUtf8Result = TryType38; +pub type FileReadUtf8ResultPayload = TryType38Payload; +pub type FileReadUtf8ResultTag = TryType38Tag; +pub type FileSizeInBytesResult = TryType44; +pub type FileSizeInBytesResultPayload = TryType44Payload; +pub type FileSizeInBytesResultTag = TryType44Tag; +pub type FileTimeAccessedResult = TryType50; +pub type FileTimeAccessedResultPayload = TryType50Payload; +pub type FileTimeAccessedResultTag = TryType50Tag; +pub type FileTimeCreatedResult = TryType50; +pub type FileTimeCreatedResultPayload = TryType50Payload; +pub type FileTimeCreatedResultTag = TryType50Tag; +pub type FileTimeModifiedResult = TryType50; +pub type FileTimeModifiedResultPayload = TryType50Payload; +pub type FileTimeModifiedResultTag = TryType50Tag; +pub type FileWriteBytesResult = TryType35; +pub type FileWriteBytesResultPayload = TryType35Payload; +pub type FileWriteBytesResultTag = TryType35Tag; +pub type FileWriteUtf8Result = TryType40; +pub type FileWriteUtf8ResultPayload = TryType40Payload; +pub type FileWriteUtf8ResultTag = TryType40Tag; +pub type HttpHostSendRequest = AnonStruct53; +pub type LocaleGetResult = TryType62; +pub type LocaleGetResultPayload = TryType62Payload; +pub type LocaleGetResultTag = TryType62Tag; +pub type PathHostPathTypeResult = TryType66; +pub type PathHostPathTypeResultPayload = TryType66Payload; +pub type PathHostPathTypeResultTag = TryType66Tag; +pub type PathIOErr = IOErrType67; +pub type PathIOErrPayload = IOErrType67Payload; +pub type PathIOErrTag = IOErrType67Tag; +pub type PathHostPathTypeOk = AnonStruct69; +pub type RandomSeedU32Result = TryType79; +pub type RandomSeedU32ResultPayload = TryType79Payload; +pub type RandomSeedU32ResultTag = TryType79Tag; +pub type RandomIOErr = IOErrType75; +pub type RandomIOErrPayload = IOErrType75Payload; +pub type RandomIOErrTag = IOErrType75Tag; +pub type RandomSeedU64Result = TryType73; +pub type RandomSeedU64ResultPayload = TryType73Payload; +pub type RandomSeedU64ResultTag = TryType73Tag; +pub type SqliteHostBindResult = TryType90; +pub type SqliteHostBindResultPayload = TryType90Payload; +pub type SqliteHostBindResultTag = TryType90Tag; +pub type SqliteHostBindErr = AnonStruct85; +pub type SqliteHostColumnValueResult = TryType99; +pub type SqliteHostColumnValueResultPayload = TryType99Payload; +pub type SqliteHostColumnValueResultTag = TryType99Tag; +pub type SqliteHostColumnValueErr = AnonStruct85; +pub type SqliteHostPrepareResult = TryType84; +pub type SqliteHostPrepareResultPayload = TryType84Payload; +pub type SqliteHostPrepareResultTag = TryType84Tag; +pub type SqliteHostPrepareErr = AnonStruct85; +pub type SqliteHostResetResult = TryType90; +pub type SqliteHostResetResultPayload = TryType90Payload; +pub type SqliteHostResetResultTag = TryType90Tag; +pub type SqliteHostResetErr = AnonStruct85; +pub type SqliteHostStepResult = TryType100; +pub type SqliteHostStepResultPayload = TryType100Payload; +pub type SqliteHostStepResultTag = TryType100Tag; +pub type SqliteHostStepErr = AnonStruct85; +pub type StderrLineResult = TryType102; +pub type StderrLineResultPayload = TryType102Payload; +pub type StderrLineResultTag = TryType102Tag; +pub type StderrIOErr = IOErrType104; +pub type StderrIOErrPayload = IOErrType104Payload; +pub type StderrIOErrTag = IOErrType104Tag; +pub type StderrWriteResult = TryType102; +pub type StderrWriteResultPayload = TryType102Payload; +pub type StderrWriteResultTag = TryType102Tag; +pub type StderrWriteBytesResult = TryType107; +pub type StderrWriteBytesResultPayload = TryType107Payload; +pub type StderrWriteBytesResultTag = TryType107Tag; +pub type StdinBytesResult = TryType116; +pub type StdinBytesResultPayload = TryType116Payload; +pub type StdinBytesResultTag = TryType116Tag; +pub type StdinIOErr = IOErrType113; +pub type StdinIOErrPayload = IOErrType113Payload; +pub type StdinIOErrTag = IOErrType113Tag; +pub type StdinLineResult = TryType111; +pub type StdinLineResultPayload = TryType111Payload; +pub type StdinLineResultTag = TryType111Tag; +pub type StdinReadToEndResult = TryType120; +pub type StdinReadToEndResultPayload = TryType120Payload; +pub type StdinReadToEndResultTag = TryType120Tag; +pub type StdoutLineResult = TryType122; +pub type StdoutLineResultPayload = TryType122Payload; +pub type StdoutLineResultTag = TryType122Tag; +pub type StdoutIOErr = IOErrType124; +pub type StdoutIOErrPayload = IOErrType124Payload; +pub type StdoutIOErrTag = IOErrType124Tag; +pub type StdoutWriteResult = TryType122; +pub type StdoutWriteResultPayload = TryType122Payload; +pub type StdoutWriteResultTag = TryType122Tag; +pub type StdoutWriteBytesResult = TryType127; +pub type StdoutWriteBytesResultPayload = TryType127Payload; +pub type StdoutWriteBytesResultTag = TryType127Tag; +pub type TcpHostConnectResult = TryType131; +pub type TcpHostConnectResultPayload = TryType131Payload; +pub type TcpHostConnectResultTag = TryType131Tag; +pub type TcpHostReadExactlyResult = TryType136; +pub type TcpHostReadExactlyResultPayload = TryType136Payload; +pub type TcpHostReadExactlyResultTag = TryType136Tag; +pub type TcpHostReadUntilResult = TryType136; +pub type TcpHostReadUntilResultPayload = TryType136Payload; +pub type TcpHostReadUntilResultTag = TryType136Tag; +pub type TcpHostReadUpToResult = TryType136; +pub type TcpHostReadUpToResultPayload = TryType136Payload; +pub type TcpHostReadUpToResultTag = TryType136Tag; +pub type TcpHostWriteResult = TryType139; +pub type TcpHostWriteResultPayload = TryType139Payload; +pub type TcpHostWriteResultTag = TryType139Tag; + +// ============================================================================= +// Generated Refcount Helpers +// ============================================================================= + +/// Recursively decrement Roc-owned payloads in TryType0. +pub fn decref_try_type0(value: TryType0, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType0Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type1(payload, roc_host); + }, + TryType0Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType0. +pub fn incref_try_type0(value: TryType0, amount: isize) { + let _ = amount; + match value.tag { + TryType0Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type1(payload, amount); + }, + TryType0Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in IOErrType1. +pub fn decref_ioerr_type1(value: IOErrType1, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrType1Tag::AlreadyExists => {}, + IOErrType1Tag::BrokenPipe => {}, + IOErrType1Tag::Interrupted => {}, + IOErrType1Tag::NotFound => {}, + IOErrType1Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrType1Tag::OutOfMemory => {}, + IOErrType1Tag::PermissionDenied => {}, + IOErrType1Tag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErrType1. +pub fn incref_ioerr_type1(value: IOErrType1, amount: isize) { + let _ = amount; + match value.tag { + IOErrType1Tag::AlreadyExists => {}, + IOErrType1Tag::BrokenPipe => {}, + IOErrType1Tag::Interrupted => {}, + IOErrType1Tag::NotFound => {}, + IOErrType1Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrType1Tag::OutOfMemory => {}, + IOErrType1Tag::PermissionDenied => {}, + IOErrType1Tag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned fields in Cmd. +pub fn decref_cmd(value: Cmd, roc_host: &RocHost) { + { + let list = value.args; + if list.has_one_ref() { + for item_ref in list.allocation_items() { + let item = *item_ref; + item.decref(roc_host); + } + } + list.decref(roc_host); + } + { + let list = value.envs; + if list.has_one_ref() { + for item_ref in list.allocation_items() { + let item = *item_ref; + item.decref(roc_host); + } + } + list.decref(roc_host); + } + value.program.decref(roc_host); +} + +/// Increment Roc-owned fields in Cmd. +pub fn incref_cmd(value: Cmd, amount: isize) { + value.args.incref(amount); + value.envs.incref(amount); + value.program.incref(amount); +} + +/// Recursively decrement Roc-owned payloads in TryType7. +pub fn decref_try_type7(value: TryType7, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType7Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_try_type8(payload, roc_host); + }, + TryType7Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + decref_anon_struct12(payload, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType7. +pub fn incref_try_type7(value: TryType7, amount: isize) { + let _ = amount; + match value.tag { + TryType7Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_try_type8(payload, amount); + }, + TryType7Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_anon_struct12(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType8. +pub fn decref_try_type8(value: TryType8, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType8Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type1(payload, roc_host); + }, + TryType8Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + decref_anon_struct9(payload, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType8. +pub fn incref_try_type8(value: TryType8, amount: isize) { + let _ = amount; + match value.tag { + TryType8Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type1(payload, amount); + }, + TryType8Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_anon_struct9(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct9. +pub fn decref_anon_struct9(value: AnonStruct9, roc_host: &RocHost) { + value.stderr_bytes.decref(roc_host); + value.stdout_bytes.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct9. +pub fn incref_anon_struct9(value: AnonStruct9, amount: isize) { + value.stderr_bytes.incref(amount); + value.stdout_bytes.incref(amount); +} + +/// Recursively decrement Roc-owned fields in AnonStruct12. +pub fn decref_anon_struct12(value: AnonStruct12, roc_host: &RocHost) { + value.stderr_bytes.decref(roc_host); + value.stdout_bytes.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct12. +pub fn incref_anon_struct12(value: AnonStruct12, amount: isize) { + value.stderr_bytes.incref(amount); + value.stdout_bytes.incref(amount); +} + +/// Recursively decrement Roc-owned payloads in TryType13. +pub fn decref_try_type13(value: TryType13, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType13Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type15(payload, roc_host); + }, + TryType13Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType13. +pub fn incref_try_type13(value: TryType13, amount: isize) { + let _ = amount; + match value.tag { + TryType13Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type15(payload, amount); + }, + TryType13Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in IOErrType15. +pub fn decref_ioerr_type15(value: IOErrType15, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrType15Tag::AlreadyExists => {}, + IOErrType15Tag::BrokenPipe => {}, + IOErrType15Tag::Interrupted => {}, + IOErrType15Tag::NotFound => {}, + IOErrType15Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrType15Tag::OutOfMemory => {}, + IOErrType15Tag::PermissionDenied => {}, + IOErrType15Tag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErrType15. +pub fn incref_ioerr_type15(value: IOErrType15, amount: isize) { + let _ = amount; + match value.tag { + IOErrType15Tag::AlreadyExists => {}, + IOErrType15Tag::BrokenPipe => {}, + IOErrType15Tag::Interrupted => {}, + IOErrType15Tag::NotFound => {}, + IOErrType15Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrType15Tag::OutOfMemory => {}, + IOErrType15Tag::PermissionDenied => {}, + IOErrType15Tag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType18. +pub fn decref_try_type18(value: TryType18, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType18Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type15(payload, roc_host); + }, + TryType18Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + { + let list = payload; + if list.has_one_ref() { + for item_ref in list.allocation_items() { + let item = *item_ref; + item.decref(roc_host); + } + } + list.decref(roc_host); + } + }, + } +} + +/// Increment Roc-owned payloads in TryType18. +pub fn incref_try_type18(value: TryType18, amount: isize) { + let _ = amount; + match value.tag { + TryType18Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type15(payload, amount); + }, + TryType18Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType21. +pub fn decref_try_type21(value: TryType21, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType21Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.decref(roc_host); + }, + TryType21Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType21. +pub fn incref_try_type21(value: TryType21, amount: isize) { + let _ = amount; + match value.tag { + TryType21Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.incref(amount); + }, + TryType21Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType24. +pub fn decref_try_type24(value: TryType24, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType24Tag::Err => {}, + TryType24Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType24. +pub fn incref_try_type24(value: TryType24, amount: isize) { + let _ = amount; + match value.tag { + TryType24Tag::Err => {}, + TryType24Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType27. +pub fn decref_try_type27(value: TryType27, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType27Tag::Err => {}, + TryType27Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType27. +pub fn incref_try_type27(value: TryType27, amount: isize) { + let _ = amount; + match value.tag { + TryType27Tag::Err => {}, + TryType27Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType29. +pub fn decref_try_type29(value: TryType29, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType29Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type31(payload, roc_host); + }, + TryType29Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType29. +pub fn incref_try_type29(value: TryType29, amount: isize) { + let _ = amount; + match value.tag { + TryType29Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type31(payload, amount); + }, + TryType29Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in IOErrType31. +pub fn decref_ioerr_type31(value: IOErrType31, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrType31Tag::AlreadyExists => {}, + IOErrType31Tag::BrokenPipe => {}, + IOErrType31Tag::Interrupted => {}, + IOErrType31Tag::NotFound => {}, + IOErrType31Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrType31Tag::OutOfMemory => {}, + IOErrType31Tag::PermissionDenied => {}, + IOErrType31Tag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErrType31. +pub fn incref_ioerr_type31(value: IOErrType31, amount: isize) { + let _ = amount; + match value.tag { + IOErrType31Tag::AlreadyExists => {}, + IOErrType31Tag::BrokenPipe => {}, + IOErrType31Tag::Interrupted => {}, + IOErrType31Tag::NotFound => {}, + IOErrType31Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrType31Tag::OutOfMemory => {}, + IOErrType31Tag::PermissionDenied => {}, + IOErrType31Tag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType35. +pub fn decref_try_type35(value: TryType35, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType35Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type31(payload, roc_host); + }, + TryType35Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType35. +pub fn incref_try_type35(value: TryType35, amount: isize) { + let _ = amount; + match value.tag { + TryType35Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type31(payload, amount); + }, + TryType35Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType38. +pub fn decref_try_type38(value: TryType38, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType38Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type31(payload, roc_host); + }, + TryType38Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType38. +pub fn incref_try_type38(value: TryType38, amount: isize) { + let _ = amount; + match value.tag { + TryType38Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type31(payload, amount); + }, + TryType38Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType40. +pub fn decref_try_type40(value: TryType40, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType40Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type31(payload, roc_host); + }, + TryType40Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType40. +pub fn incref_try_type40(value: TryType40, amount: isize) { + let _ = amount; + match value.tag { + TryType40Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type31(payload, amount); + }, + TryType40Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType42. +pub fn decref_try_type42(value: TryType42, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType42Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type31(payload, roc_host); + }, + TryType42Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType42. +pub fn incref_try_type42(value: TryType42, amount: isize) { + let _ = amount; + match value.tag { + TryType42Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type31(payload, amount); + }, + TryType42Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType44. +pub fn decref_try_type44(value: TryType44, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType44Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type31(payload, roc_host); + }, + TryType44Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType44. +pub fn incref_try_type44(value: TryType44, amount: isize) { + let _ = amount; + match value.tag { + TryType44Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type31(payload, amount); + }, + TryType44Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType47. +pub fn decref_try_type47(value: TryType47, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType47Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type31(payload, roc_host); + }, + TryType47Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType47. +pub fn incref_try_type47(value: TryType47, amount: isize) { + let _ = amount; + match value.tag { + TryType47Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type31(payload, amount); + }, + TryType47Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType50. +pub fn decref_try_type50(value: TryType50, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType50Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type31(payload, roc_host); + }, + TryType50Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType50. +pub fn incref_try_type50(value: TryType50, amount: isize) { + let _ = amount; + match value.tag { + TryType50Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type31(payload, amount); + }, + TryType50Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct53. +pub fn decref_anon_struct53(value: AnonStruct53, roc_host: &RocHost) { + value.body.decref(roc_host); + { + let list = value.headers; + if list.has_one_ref() { + for item_ref in list.allocation_items() { + let item = *item_ref; + decref_anon_struct57(item, roc_host); + } + } + list.decref(roc_host); + } +} + +/// Increment Roc-owned fields in AnonStruct53. +pub fn incref_anon_struct53(value: AnonStruct53, amount: isize) { + value.body.incref(amount); + value.headers.incref(amount); +} + +/// Recursively decrement Roc-owned fields in AnonStruct57. +pub fn decref_anon_struct57(value: AnonStruct57, roc_host: &RocHost) { + value.name.decref(roc_host); + value.value.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct57. +pub fn incref_anon_struct57(value: AnonStruct57, amount: isize) { + value.name.incref(amount); + value.value.incref(amount); +} + +/// Recursively decrement Roc-owned fields in AnonStruct60. +pub fn decref_anon_struct60(value: AnonStruct60, roc_host: &RocHost) { + value.body.decref(roc_host); + { + let list = value.headers; + if list.has_one_ref() { + for item_ref in list.allocation_items() { + let item = *item_ref; + decref_anon_struct57(item, roc_host); + } + } + list.decref(roc_host); + } + value.method_ext.decref(roc_host); + value.uri.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct60. +pub fn incref_anon_struct60(value: AnonStruct60, amount: isize) { + value.body.incref(amount); + value.headers.incref(amount); + value.method_ext.incref(amount); + value.uri.incref(amount); +} + +/// Recursively decrement Roc-owned payloads in TryType62. +pub fn decref_try_type62(value: TryType62, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType62Tag::Err => {}, + TryType62Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType62. +pub fn incref_try_type62(value: TryType62, amount: isize) { + let _ = amount; + match value.tag { + TryType62Tag::Err => {}, + TryType62Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType66. +pub fn decref_try_type66(value: TryType66, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType66Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type67(payload, roc_host); + }, + TryType66Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + decref_anon_struct69(payload, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType66. +pub fn incref_try_type66(value: TryType66, amount: isize) { + let _ = amount; + match value.tag { + TryType66Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type67(payload, amount); + }, + TryType66Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_anon_struct69(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in IOErrType67. +pub fn decref_ioerr_type67(value: IOErrType67, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrType67Tag::AlreadyExists => {}, + IOErrType67Tag::BrokenPipe => {}, + IOErrType67Tag::Interrupted => {}, + IOErrType67Tag::NotFound => {}, + IOErrType67Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrType67Tag::OutOfMemory => {}, + IOErrType67Tag::PermissionDenied => {}, + IOErrType67Tag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErrType67. +pub fn incref_ioerr_type67(value: IOErrType67, amount: isize) { + let _ = amount; + match value.tag { + IOErrType67Tag::AlreadyExists => {}, + IOErrType67Tag::BrokenPipe => {}, + IOErrType67Tag::Interrupted => {}, + IOErrType67Tag::NotFound => {}, + IOErrType67Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrType67Tag::OutOfMemory => {}, + IOErrType67Tag::PermissionDenied => {}, + IOErrType67Tag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct69. +pub fn decref_anon_struct69(value: AnonStruct69, roc_host: &RocHost) { + let _ = value; + let _ = roc_host; +} + +/// Increment Roc-owned fields in AnonStruct69. +pub fn incref_anon_struct69(value: AnonStruct69, amount: isize) { + let _ = value; + let _ = amount; +} + +/// Recursively decrement Roc-owned payloads in TryType73. +pub fn decref_try_type73(value: TryType73, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType73Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type75(payload, roc_host); + }, + TryType73Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType73. +pub fn incref_try_type73(value: TryType73, amount: isize) { + let _ = amount; + match value.tag { + TryType73Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type75(payload, amount); + }, + TryType73Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in IOErrType75. +pub fn decref_ioerr_type75(value: IOErrType75, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrType75Tag::AlreadyExists => {}, + IOErrType75Tag::BrokenPipe => {}, + IOErrType75Tag::Interrupted => {}, + IOErrType75Tag::NotFound => {}, + IOErrType75Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrType75Tag::OutOfMemory => {}, + IOErrType75Tag::PermissionDenied => {}, + IOErrType75Tag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErrType75. +pub fn incref_ioerr_type75(value: IOErrType75, amount: isize) { + let _ = amount; + match value.tag { + IOErrType75Tag::AlreadyExists => {}, + IOErrType75Tag::BrokenPipe => {}, + IOErrType75Tag::Interrupted => {}, + IOErrType75Tag::NotFound => {}, + IOErrType75Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrType75Tag::OutOfMemory => {}, + IOErrType75Tag::PermissionDenied => {}, + IOErrType75Tag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType79. +pub fn decref_try_type79(value: TryType79, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType79Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type75(payload, roc_host); + }, + TryType79Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType79. +pub fn incref_try_type79(value: TryType79, amount: isize) { + let _ = amount; + match value.tag { + TryType79Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type75(payload, amount); + }, + TryType79Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType84. +pub fn decref_try_type84(value: TryType84, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType84Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_anon_struct85(payload, roc_host); + }, + TryType84Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + decref_box_with(payload as RocBox, core::mem::align_of::(), false, None, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType84. +pub fn incref_try_type84(value: TryType84, amount: isize) { + let _ = amount; + match value.tag { + TryType84Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_anon_struct85(payload, amount); + }, + TryType84Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_box(payload as RocBox, amount); + }, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct85. +pub fn decref_anon_struct85(value: AnonStruct85, roc_host: &RocHost) { + value.message.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct85. +pub fn incref_anon_struct85(value: AnonStruct85, amount: isize) { + value.message.incref(amount); +} + +/// Recursively decrement Roc-owned payloads in TryType90. +pub fn decref_try_type90(value: TryType90, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType90Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_anon_struct85(payload, roc_host); + }, + TryType90Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType90. +pub fn incref_try_type90(value: TryType90, amount: isize) { + let _ = amount; + match value.tag { + TryType90Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_anon_struct85(payload, amount); + }, + TryType90Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct93. +pub fn decref_anon_struct93(value: AnonStruct93, roc_host: &RocHost) { + value.name.decref(roc_host); + decref_bytes_or_integer_or_null_or_real_or_string(value.value, roc_host); +} + +/// Increment Roc-owned fields in AnonStruct93. +pub fn incref_anon_struct93(value: AnonStruct93, amount: isize) { + value.name.incref(amount); + incref_bytes_or_integer_or_null_or_real_or_string(value.value, amount); +} + +/// Recursively decrement Roc-owned payloads in BytesOrIntegerOrNullOrRealOrString. +pub fn decref_bytes_or_integer_or_null_or_real_or_string(value: BytesOrIntegerOrNullOrRealOrString, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + BytesOrIntegerOrNullOrRealOrStringTag::Bytes => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.bytes); + payload.decref(roc_host); + }, + BytesOrIntegerOrNullOrRealOrStringTag::Integer => {}, + BytesOrIntegerOrNullOrRealOrStringTag::Null => {}, + BytesOrIntegerOrNullOrRealOrStringTag::Real => {}, + BytesOrIntegerOrNullOrRealOrStringTag::String => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.string); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in BytesOrIntegerOrNullOrRealOrString. +pub fn incref_bytes_or_integer_or_null_or_real_or_string(value: BytesOrIntegerOrNullOrRealOrString, amount: isize) { + let _ = amount; + match value.tag { + BytesOrIntegerOrNullOrRealOrStringTag::Bytes => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.bytes); + payload.incref(amount); + }, + BytesOrIntegerOrNullOrRealOrStringTag::Integer => {}, + BytesOrIntegerOrNullOrRealOrStringTag::Null => {}, + BytesOrIntegerOrNullOrRealOrStringTag::Real => {}, + BytesOrIntegerOrNullOrRealOrStringTag::String => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.string); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType99. +pub fn decref_try_type99(value: TryType99, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType99Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_anon_struct85(payload, roc_host); + }, + TryType99Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + decref_bytes_or_integer_or_null_or_real_or_string(payload, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType99. +pub fn incref_try_type99(value: TryType99, amount: isize) { + let _ = amount; + match value.tag { + TryType99Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_anon_struct85(payload, amount); + }, + TryType99Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_bytes_or_integer_or_null_or_real_or_string(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType100. +pub fn decref_try_type100(value: TryType100, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType100Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_anon_struct85(payload, roc_host); + }, + TryType100Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType100. +pub fn incref_try_type100(value: TryType100, amount: isize) { + let _ = amount; + match value.tag { + TryType100Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_anon_struct85(payload, amount); + }, + TryType100Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType102. +pub fn decref_try_type102(value: TryType102, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType102Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type104(payload, roc_host); + }, + TryType102Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType102. +pub fn incref_try_type102(value: TryType102, amount: isize) { + let _ = amount; + match value.tag { + TryType102Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type104(payload, amount); + }, + TryType102Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in IOErrType104. +pub fn decref_ioerr_type104(value: IOErrType104, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrType104Tag::AlreadyExists => {}, + IOErrType104Tag::BrokenPipe => {}, + IOErrType104Tag::Interrupted => {}, + IOErrType104Tag::NotFound => {}, + IOErrType104Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrType104Tag::OutOfMemory => {}, + IOErrType104Tag::PermissionDenied => {}, + IOErrType104Tag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErrType104. +pub fn incref_ioerr_type104(value: IOErrType104, amount: isize) { + let _ = amount; + match value.tag { + IOErrType104Tag::AlreadyExists => {}, + IOErrType104Tag::BrokenPipe => {}, + IOErrType104Tag::Interrupted => {}, + IOErrType104Tag::NotFound => {}, + IOErrType104Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrType104Tag::OutOfMemory => {}, + IOErrType104Tag::PermissionDenied => {}, + IOErrType104Tag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType107. +pub fn decref_try_type107(value: TryType107, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType107Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type104(payload, roc_host); + }, + TryType107Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType107. +pub fn incref_try_type107(value: TryType107, amount: isize) { + let _ = amount; + match value.tag { + TryType107Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type104(payload, amount); + }, + TryType107Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType111. +pub fn decref_try_type111(value: TryType111, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType111Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_end_of_file_or_stdin_err_type112(payload, roc_host); + }, + TryType111Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType111. +pub fn incref_try_type111(value: TryType111, amount: isize) { + let _ = amount; + match value.tag { + TryType111Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_end_of_file_or_stdin_err_type112(payload, amount); + }, + TryType111Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in EndOfFileOrStdinErrType112. +pub fn decref_end_of_file_or_stdin_err_type112(value: EndOfFileOrStdinErrType112, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + EndOfFileOrStdinErrType112Tag::EndOfFile => {}, + EndOfFileOrStdinErrType112Tag::StdinErr => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.stdin_err); + decref_ioerr_type113(payload, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in EndOfFileOrStdinErrType112. +pub fn incref_end_of_file_or_stdin_err_type112(value: EndOfFileOrStdinErrType112, amount: isize) { + let _ = amount; + match value.tag { + EndOfFileOrStdinErrType112Tag::EndOfFile => {}, + EndOfFileOrStdinErrType112Tag::StdinErr => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.stdin_err); + incref_ioerr_type113(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in IOErrType113. +pub fn decref_ioerr_type113(value: IOErrType113, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrType113Tag::AlreadyExists => {}, + IOErrType113Tag::BrokenPipe => {}, + IOErrType113Tag::Interrupted => {}, + IOErrType113Tag::NotFound => {}, + IOErrType113Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrType113Tag::OutOfMemory => {}, + IOErrType113Tag::PermissionDenied => {}, + IOErrType113Tag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErrType113. +pub fn incref_ioerr_type113(value: IOErrType113, amount: isize) { + let _ = amount; + match value.tag { + IOErrType113Tag::AlreadyExists => {}, + IOErrType113Tag::BrokenPipe => {}, + IOErrType113Tag::Interrupted => {}, + IOErrType113Tag::NotFound => {}, + IOErrType113Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrType113Tag::OutOfMemory => {}, + IOErrType113Tag::PermissionDenied => {}, + IOErrType113Tag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType116. +pub fn decref_try_type116(value: TryType116, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType116Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_end_of_file_or_stdin_err_type117(payload, roc_host); + }, + TryType116Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType116. +pub fn incref_try_type116(value: TryType116, amount: isize) { + let _ = amount; + match value.tag { + TryType116Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_end_of_file_or_stdin_err_type117(payload, amount); + }, + TryType116Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in EndOfFileOrStdinErrType117. +pub fn decref_end_of_file_or_stdin_err_type117(value: EndOfFileOrStdinErrType117, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + EndOfFileOrStdinErrType117Tag::EndOfFile => {}, + EndOfFileOrStdinErrType117Tag::StdinErr => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.stdin_err); + decref_ioerr_type113(payload, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in EndOfFileOrStdinErrType117. +pub fn incref_end_of_file_or_stdin_err_type117(value: EndOfFileOrStdinErrType117, amount: isize) { + let _ = amount; + match value.tag { + EndOfFileOrStdinErrType117Tag::EndOfFile => {}, + EndOfFileOrStdinErrType117Tag::StdinErr => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.stdin_err); + incref_ioerr_type113(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType120. +pub fn decref_try_type120(value: TryType120, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType120Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type113(payload, roc_host); + }, + TryType120Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType120. +pub fn incref_try_type120(value: TryType120, amount: isize) { + let _ = amount; + match value.tag { + TryType120Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type113(payload, amount); + }, + TryType120Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType122. +pub fn decref_try_type122(value: TryType122, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType122Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type124(payload, roc_host); + }, + TryType122Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType122. +pub fn incref_try_type122(value: TryType122, amount: isize) { + let _ = amount; + match value.tag { + TryType122Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type124(payload, amount); + }, + TryType122Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in IOErrType124. +pub fn decref_ioerr_type124(value: IOErrType124, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrType124Tag::AlreadyExists => {}, + IOErrType124Tag::BrokenPipe => {}, + IOErrType124Tag::Interrupted => {}, + IOErrType124Tag::NotFound => {}, + IOErrType124Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrType124Tag::OutOfMemory => {}, + IOErrType124Tag::PermissionDenied => {}, + IOErrType124Tag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErrType124. +pub fn incref_ioerr_type124(value: IOErrType124, amount: isize) { + let _ = amount; + match value.tag { + IOErrType124Tag::AlreadyExists => {}, + IOErrType124Tag::BrokenPipe => {}, + IOErrType124Tag::Interrupted => {}, + IOErrType124Tag::NotFound => {}, + IOErrType124Tag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrType124Tag::OutOfMemory => {}, + IOErrType124Tag::PermissionDenied => {}, + IOErrType124Tag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType127. +pub fn decref_try_type127(value: TryType127, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType127Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr_type124(payload, roc_host); + }, + TryType127Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType127. +pub fn incref_try_type127(value: TryType127, amount: isize) { + let _ = amount; + match value.tag { + TryType127Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr_type124(payload, amount); + }, + TryType127Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType131. +pub fn decref_try_type131(value: TryType131, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType131Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.decref(roc_host); + }, + TryType131Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + decref_box_with(payload as RocBox, core::mem::align_of::(), false, None, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType131. +pub fn incref_try_type131(value: TryType131, amount: isize) { + let _ = amount; + match value.tag { + TryType131Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.incref(amount); + }, + TryType131Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_box(payload as RocBox, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType136. +pub fn decref_try_type136(value: TryType136, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType136Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.decref(roc_host); + }, + TryType136Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType136. +pub fn incref_try_type136(value: TryType136, amount: isize) { + let _ = amount; + match value.tag { + TryType136Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.incref(amount); + }, + TryType136Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType139. +pub fn decref_try_type139(value: TryType139, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType139Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.decref(roc_host); + }, + TryType139Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType139. +pub fn incref_try_type139(value: TryType139, amount: isize) { + let _ = amount; + match value.tag { + TryType139Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.incref(amount); + }, + TryType139Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType147. +pub fn decref_try_type147(value: TryType147, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType147Tag::Err => {}, + TryType147Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType147. +pub fn incref_try_type147(value: TryType147, amount: isize) { + let _ = amount; + match value.tag { + TryType147Tag::Err => {}, + TryType147Tag::Ok => {}, + } +} + + +// ============================================================================= +// Runtime Symbols +// +// The host defines these linker symbols. Compiled Roc code calls them directly. +// ============================================================================= + +#[allow(improper_ctypes)] +unsafe extern "C" { + pub fn roc_alloc(length: usize, alignment: usize) -> *mut c_void; + pub fn roc_dealloc(ptr: *mut c_void, alignment: usize); + pub fn roc_realloc(ptr: *mut c_void, new_length: usize, alignment: usize) -> *mut c_void; + pub fn roc_dbg(bytes: *const u8, len: usize); + pub fn roc_expect_failed(bytes: *const u8, len: usize); + pub fn roc_crashed(bytes: *const u8, len: usize); +} + +// ============================================================================= +// Hosted Symbols +// +// The platform host must export these symbols with the exact direct C ABI signatures. +// Refcounted arguments are owned by the hosted function. +// ============================================================================= + +#[allow(improper_ctypes)] +unsafe extern "C" { + /// Hosted symbol for Cmd.host_exec_exit_code! + /// Roc signature: Cmd => Try(I32, IOErr) + pub fn hosted_cmd_host_exec_exit_code(arg0: Cmd) -> TryType0; + + /// Hosted symbol for Cmd.host_exec_output! + /// Roc signature: Cmd => Try({ stderr_bytes : List(U8), stdout_bytes : List(U8) }, Try({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) }, IOErr)) + pub fn hosted_cmd_host_exec_output(arg0: Cmd) -> TryType7; + + /// Hosted symbol for Dir.create! + /// Roc signature: Str => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_create(arg0: RocStr) -> TryType13; + + /// Hosted symbol for Dir.create_all! + /// Roc signature: Str => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_create_all(arg0: RocStr) -> TryType13; + + /// Hosted symbol for Dir.delete_all! + /// Roc signature: Str => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_delete_all(arg0: RocStr) -> TryType13; + + /// Hosted symbol for Dir.delete_empty! + /// Roc signature: Str => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_delete_empty(arg0: RocStr) -> TryType13; + + /// Hosted symbol for Dir.list! + /// Roc signature: Str => Try(List(Str), [DirErr(IOErr)]) + pub fn hosted_dir_list(arg0: RocStr) -> TryType18; + + /// Hosted symbol for Env.cwd! + /// Roc signature: {} => Try(Str, [CwdUnavailable]) + pub fn hosted_env_cwd() -> TryType24; + + /// Hosted symbol for Env.exe_path! + /// Roc signature: {} => Try(Str, [ExePathUnavailable]) + pub fn hosted_env_exe_path() -> TryType27; + + /// Hosted symbol for Env.temp_dir! + /// Roc signature: {} => Str + pub fn hosted_env_temp_dir() -> RocStr; + + /// Hosted symbol for Env.var! + /// Roc signature: Str => Try(Str, [VarNotFound(Str)]) + pub fn hosted_env_var(arg0: RocStr) -> TryType21; + + /// Hosted symbol for File.delete! + /// Roc signature: Str => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_delete(arg0: RocStr) -> TryType42; + + /// Hosted symbol for File.is_executable! + /// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_executable(arg0: RocStr) -> TryType47; + + /// Hosted symbol for File.is_readable! + /// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_readable(arg0: RocStr) -> TryType47; + + /// Hosted symbol for File.is_writable! + /// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_writable(arg0: RocStr) -> TryType47; + + /// Hosted symbol for File.read_bytes! + /// Roc signature: Str => Try(List(U8), [FileErr(IOErr)]) + pub fn hosted_file_read_bytes(arg0: RocStr) -> TryType29; + + /// Hosted symbol for File.read_utf8! + /// Roc signature: Str => Try(Str, [FileErr(IOErr)]) + pub fn hosted_file_read_utf8(arg0: RocStr) -> TryType38; + + /// Hosted symbol for File.size_in_bytes! + /// Roc signature: Str => Try(U64, [FileErr(IOErr)]) + pub fn hosted_file_size_in_bytes(arg0: RocStr) -> TryType44; + + /// Hosted symbol for File.time_accessed! + /// Roc signature: Str => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_accessed(arg0: RocStr) -> TryType50; + + /// Hosted symbol for File.time_created! + /// Roc signature: Str => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_created(arg0: RocStr) -> TryType50; + + /// Hosted symbol for File.time_modified! + /// Roc signature: Str => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_modified(arg0: RocStr) -> TryType50; + + /// Hosted symbol for File.write_bytes! + /// Roc signature: Str, List(U8) => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_write_bytes(arg0: RocStr, arg1: RocListWith) -> TryType35; + + /// Hosted symbol for File.write_utf8! + /// Roc signature: Str, Str => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_write_utf8(arg0: RocStr, arg1: RocStr) -> TryType40; + + /// Hosted symbol for Http.host_send_request! + /// Roc signature: { body : List(U8), headers : List({ name : Str, value : Str }), method : U64, method_ext : Str, timeout_ms : U64, uri : Str } => { body : List(U8), headers : List({ name : Str, value : Str }), status : U16 } + pub fn hosted_http_send_request(arg0: HttpHostSendRequestArgs) -> AnonStruct53; + + /// Hosted symbol for Locale.all! + /// Roc signature: {} => List(Str) + pub fn hosted_locale_all() -> RocList; + + /// Hosted symbol for Locale.get! + /// Roc signature: {} => Try(Str, [NotAvailable]) + pub fn hosted_locale_get() -> TryType62; + + /// Hosted symbol for Path.host_path_type! + /// Roc signature: List(U8) => Try({ is_dir : Bool, is_file : Bool, is_sym_link : Bool }, IOErr) + pub fn hosted_path_type(arg0: RocListWith) -> TryType66; + + /// Hosted symbol for Random.seed_u32! + /// Roc signature: {} => Try(U32, [RandomErr(IOErr)]) + pub fn hosted_random_seed_u32() -> TryType79; + + /// Hosted symbol for Random.seed_u64! + /// Roc signature: {} => Try(U64, [RandomErr(IOErr)]) + pub fn hosted_random_seed_u64() -> TryType73; + + /// Hosted symbol for Sleep.millis! + /// Roc signature: U64 => {} + pub fn hosted_sleep_millis(arg0: u64); + + /// Hosted symbol for Sqlite.host_bind! + /// Roc signature: Sqlite.Stmt, List({ name : Str, value : [Bytes(List(U8)), Integer(I64), Null, Real(F64), String(Str)] }) => Try({}, { code : I64, message : Str }) + pub fn hosted_sqlite_bind(arg0: *mut u64, arg1: RocList) -> TryType90; + + /// Hosted symbol for Sqlite.host_column_value! + /// Roc signature: Sqlite.Stmt, U64 => Try([Bytes(List(U8)), Integer(I64), Null, Real(F64), String(Str)], { code : I64, message : Str }) + pub fn hosted_sqlite_column_value(arg0: *mut u64, arg1: u64) -> TryType99; + + /// Hosted symbol for Sqlite.host_columns! + /// Roc signature: Sqlite.Stmt => List(Str) + pub fn hosted_sqlite_columns(arg0: *mut u64) -> RocList; + + /// Hosted symbol for Sqlite.host_prepare! + /// Roc signature: Str, Str => Try(Sqlite.Stmt, { code : I64, message : Str }) + pub fn hosted_sqlite_prepare(arg0: RocStr, arg1: RocStr) -> TryType84; + + /// Hosted symbol for Sqlite.host_reset! + /// Roc signature: Sqlite.Stmt => Try({}, { code : I64, message : Str }) + pub fn hosted_sqlite_reset(arg0: *mut u64) -> TryType90; + + /// Hosted symbol for Sqlite.host_step! + /// Roc signature: Sqlite.Stmt => Try(Bool, { code : I64, message : Str }) + pub fn hosted_sqlite_step(arg0: *mut u64) -> TryType100; + + /// Hosted symbol for Stderr.line! + /// Roc signature: Str => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_line(arg0: RocStr) -> TryType102; + + /// Hosted symbol for Stderr.write! + /// Roc signature: Str => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_write(arg0: RocStr) -> TryType102; + + /// Hosted symbol for Stderr.write_bytes! + /// Roc signature: List(U8) => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_write_bytes(arg0: RocListWith) -> TryType107; + + /// Hosted symbol for Stdin.bytes! + /// Roc signature: {} => Try(List(U8), [EndOfFile, StdinErr(IOErr)]) + pub fn hosted_stdin_bytes() -> TryType116; + + /// Hosted symbol for Stdin.line! + /// Roc signature: {} => Try(Str, [EndOfFile, StdinErr(IOErr)]) + pub fn hosted_stdin_line() -> TryType111; + + /// Hosted symbol for Stdin.read_to_end! + /// Roc signature: {} => Try(List(U8), [StdinErr(IOErr)]) + pub fn hosted_stdin_read_to_end() -> TryType120; + + /// Hosted symbol for Stdout.line! + /// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_line(arg0: RocStr) -> TryType122; + + /// Hosted symbol for Stdout.write! + /// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_write(arg0: RocStr) -> TryType122; + + /// Hosted symbol for Stdout.write_bytes! + /// Roc signature: List(U8) => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_write_bytes(arg0: RocListWith) -> TryType127; + + /// Hosted symbol for Tcp.host_connect! + /// Roc signature: Str, U16 => Try(Tcp.Stream, Str) + pub fn hosted_tcp_connect(arg0: RocStr, arg1: u16) -> TryType131; + + /// Hosted symbol for Tcp.host_read_exactly! + /// Roc signature: Tcp.Stream, U64 => Try(List(U8), Str) + pub fn hosted_tcp_read_exactly(arg0: *mut u64, arg1: u64) -> TryType136; + + /// Hosted symbol for Tcp.host_read_until! + /// Roc signature: Tcp.Stream, U8 => Try(List(U8), Str) + pub fn hosted_tcp_read_until(arg0: *mut u64, arg1: u8) -> TryType136; + + /// Hosted symbol for Tcp.host_read_up_to! + /// Roc signature: Tcp.Stream, U64 => Try(List(U8), Str) + pub fn hosted_tcp_read_up_to(arg0: *mut u64, arg1: u64) -> TryType136; + + /// Hosted symbol for Tcp.host_write! + /// Roc signature: Tcp.Stream, List(U8) => Try({}, Str) + pub fn hosted_tcp_write(arg0: *mut u64, arg1: RocListWith) -> TryType139; + + /// Hosted symbol for Tty.disable_raw_mode! + /// Roc signature: {} => {} + pub fn hosted_tty_disable_raw_mode(); + + /// Hosted symbol for Tty.enable_raw_mode! + /// Roc signature: {} => {} + pub fn hosted_tty_enable_raw_mode(); + + /// Hosted symbol for Utc.now! + /// Roc signature: {} => U128 + pub fn hosted_utc_now() -> u128; + +} + +/// Default memory management functions for Roc platform helpers. +/// +/// Memory layout: each allocation prepends size metadata so that dealloc/realloc +/// can recover the original allocation size because `roc_dealloc` receives no length. +pub struct DefaultAllocators; + +impl DefaultAllocators { + /// Allocate memory using the Rust global allocator. + pub extern "C" fn roc_alloc(_roc_host: *mut RocHost, length: usize, alignment: usize) -> *mut c_void { + unsafe { + let min_alignment = alignment.max(core::mem::align_of::()); + let size_storage_bytes = min_alignment; + let total_size = length + size_storage_bytes; + + debug_assert!(min_alignment.is_power_of_two(), "alignment must be a power of two"); + let layout = Layout::from_size_align_unchecked(total_size, min_alignment); + let base_ptr = std::alloc::alloc(layout); + if base_ptr.is_null() { + eprintln!("roc_alloc: out of memory"); + std::process::exit(1); + } + + let size_ptr = base_ptr.add(size_storage_bytes).sub(core::mem::size_of::()) as *mut usize; + *size_ptr = total_size; + + base_ptr.add(size_storage_bytes) as *mut c_void + } + } + + /// Free memory previously allocated by `roc_alloc`. + pub extern "C" fn roc_dealloc(_roc_host: *mut RocHost, ptr: *mut c_void, alignment: usize) { + unsafe { + let min_alignment = alignment.max(core::mem::align_of::()); + let size_storage_bytes = min_alignment; + + let size_ptr = (ptr as *const u8).sub(core::mem::size_of::()) as *const usize; + let total_size = *size_ptr; + + let base_ptr = (ptr as *mut u8).sub(size_storage_bytes); + debug_assert!(min_alignment.is_power_of_two(), "alignment must be a power of two"); + let layout = Layout::from_size_align_unchecked(total_size, min_alignment); + std::alloc::dealloc(base_ptr, layout); + } + } + + /// Reallocate memory, preserving existing user data. + pub extern "C" fn roc_realloc(_roc_host: *mut RocHost, ptr: *mut c_void, new_length: usize, alignment: usize) -> *mut c_void { + unsafe { + let min_alignment = alignment.max(core::mem::align_of::()); + let size_storage_bytes = min_alignment; + + let old_size_ptr = (ptr as *const u8).sub(core::mem::size_of::()) as *const usize; + let old_total_size = *old_size_ptr; + let old_base_ptr = (ptr as *mut u8).sub(size_storage_bytes); + + let new_total_size = new_length + size_storage_bytes; + debug_assert!(min_alignment.is_power_of_two(), "alignment must be a power of two"); + let old_layout = Layout::from_size_align_unchecked(old_total_size, min_alignment); + let new_base_ptr = std::alloc::realloc(old_base_ptr, old_layout, new_total_size); + if new_base_ptr.is_null() { + eprintln!("roc_realloc: out of memory"); + std::process::exit(1); + } + + let new_user_ptr = new_base_ptr.add(size_storage_bytes); + let new_size_ptr = new_user_ptr.sub(core::mem::size_of::()) as *mut usize; + *new_size_ptr = new_total_size; + new_user_ptr as *mut c_void + } + } +} + +/// Default handlers for dbg, expect-failed, and crash. +pub struct DefaultHandlers; + +impl DefaultHandlers { + /// Print a `dbg` expression to stderr. + pub extern "C" fn roc_dbg(_roc_host: *mut RocHost, bytes: *const u8, len: usize) { + unsafe { + let msg = core::slice::from_raw_parts(bytes, len); + let msg = core::str::from_utf8_unchecked(msg); + eprintln!("[ROC DBG] {}", msg); + } + } + + /// Print a failed `expect` to stderr. + pub extern "C" fn roc_expect_failed(_roc_host: *mut RocHost, bytes: *const u8, len: usize) { + unsafe { + let msg = core::slice::from_raw_parts(bytes, len); + let msg = core::str::from_utf8_unchecked(msg); + eprintln!("[ROC EXPECT] {}", msg); + } + } + + /// Print a `crash` message to stderr and exit. + pub extern "C" fn roc_crashed(_roc_host: *mut RocHost, bytes: *const u8, len: usize) { + unsafe { + let msg = core::slice::from_raw_parts(bytes, len); + let msg = core::str::from_utf8_unchecked(msg); + eprintln!("[ROC CRASHED] {}", msg); + std::process::exit(1); + } + } +} + +/// Create a host-internal helper context with default memory management and handlers. +/// +/// This is only for helper functions in this generated file. It is not passed to +/// compiled Roc code, which uses the direct symbol ABI declared above. +pub fn make_roc_host(env: *mut c_void) -> RocHost { + RocHost { + env, + roc_alloc: DefaultAllocators::roc_alloc, + roc_dealloc: DefaultAllocators::roc_dealloc, + roc_realloc: DefaultAllocators::roc_realloc, + roc_dbg: DefaultHandlers::roc_dbg, + roc_expect_failed: DefaultHandlers::roc_expect_failed, + roc_crashed: DefaultHandlers::roc_crashed, + } +} + +// ============================================================================= +// Provided Symbols +// +// Roc exports these symbols from the app with their natural C ABI signatures. +// ============================================================================= + +#[allow(improper_ctypes)] +unsafe extern "C" { + /// Entrypoint: main_for_host! + pub fn roc_main(arg0: RocList) -> i32; + +} diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index 978b1a39..00000000 --- a/tests/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Ignore all files including binary files that have no extension -* -# Unignore all files with extensions -!*.* -# Unignore all directories -!*/ \ No newline at end of file diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index a6e94d3e..00000000 --- a/tests/README.md +++ /dev/null @@ -1 +0,0 @@ -Currently most things are tested in the examples folder, if something does not fit well as an example we put it here. \ No newline at end of file diff --git a/tests/cmd-test.roc b/tests/cmd-test.roc index 323b70c4..773fdf47 100644 --- a/tests/cmd-test.roc +++ b/tests/cmd-test.roc @@ -2,118 +2,100 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.Cmd -import pf.Arg exposing [Arg] # Tests all error cases in Cmd functions. -main! : List Arg => Result {} _ -main! = |_args| +main! = |_args| { # exec! expect_err( Cmd.exec!("blablaXYZ", []), - "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" + "Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" )? expect_err( Cmd.exec!("cat", ["non_existent.txt"]), - "(Err (ExecFailed {command: \"cat non_existent.txt\", exit_code: 1}))" + "Err(ExecFailed({ command: \"cat non_existent.txt\", exit_code: 1 }))" )? # exec_cmd! expect_err( - Cmd.new("blablaXYZ") - |> Cmd.exec_cmd!, - "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" + Cmd.new("blablaXYZ").exec_cmd!(), + "Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" )? expect_err( - Cmd.new("cat") - |> Cmd.arg("non_existent.txt") - |> Cmd.exec_cmd!, - "(Err (ExecCmdFailed {command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1}))" + Cmd.new("cat").arg("non_existent.txt").exec_cmd!(), + "Err(ExecCmdFailed({ command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1 }))" )? # exec_output! expect_err( - Cmd.new("blablaXYZ") - |> Cmd.exec_output!, - "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" + Cmd.new("blablaXYZ").exec_output!(), + "Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" )? expect_err( - Cmd.new("cat") - |> Cmd.arg("non_existent.txt") - |> Cmd.exec_output!, - "(Err (NonZeroExitCode {command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1, stderr_utf8_lossy: \"cat: non_existent.txt: No such file or directory\n\", stdout_utf8_lossy: \"\"}))" + Cmd.new("cat").arg("non_existent.txt").exec_output!(), + "Err(NonZeroExitCode({ command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1, stderr_utf8_lossy: \"cat: non_existent.txt: No such file or directory\n\", stdout_utf8_lossy: \"\" }))" )? - # Test StdoutContainsInvalidUtf8 - using printf to output invalid UTF-8 bytes + # Test StdoutContainsInvalidUtf8 - blocked by compiler bug expect_err( - Cmd.new("printf") - |> Cmd.args(["\\377\\376"]) # Invalid UTF-8 sequence - |> Cmd.exec_output!, - "(Err (StdoutContainsInvalidUtf8 {cmd_str: \"{ cmd: printf, args: \\377\\376 }\", err: (BadUtf8 {index: 0, problem: InvalidStartByte})}))" + Cmd.new("printf").args(["\\377\\376"]).exec_output!(), + "Err(StdoutContainsInvalidUtf8({ err: BadUtf8({ index: 0, problem: InvalidStartByte }), cmd_str: \"{ cmd: printf, args: \\\\377\\\\376 }\" }))" )? # exec_output_bytes! expect_err( - Cmd.new("blablaXYZ") - |> Cmd.exec_output_bytes!, - "(Err (FailedToGetExitCodeB NotFound))" + Cmd.new("blablaXYZ").exec_output_bytes!(), + "Err(FailedToGetExitCodeB(NotFound))" )? expect_err( - Cmd.new("cat") - |> Cmd.arg("non_existent.txt") - |> Cmd.exec_output_bytes!, - "(Err (NonZeroExitCodeB {exit_code: 1, stderr_bytes: [99, 97, 116, 58, 32, 110, 111, 110, 95, 101, 120, 105, 115, 116, 101, 110, 116, 46, 116, 120, 116, 58, 32, 78, 111, 32, 115, 117, 99, 104, 32, 102, 105, 108, 101, 32, 111, 114, 32, 100, 105, 114, 101, 99, 116, 111, 114, 121, 10], stdout_bytes: []}))" + Cmd.new("cat").arg("non_existent.txt").exec_output_bytes!(), + "Err(NonZeroExitCodeB({ exit_code: 1, stderr_bytes: [99, 97, 116, 58, 32, 110, 111, 110, 95, 101, 120, 105, 115, 116, 101, 110, 116, 46, 116, 120, 116, 58, 32, 78, 111, 32, 115, 117, 99, 104, 32, 102, 105, 108, 101, 32, 111, 114, 32, 100, 105, 114, 101, 99, 116, 111, 114, 121, 10], stdout_bytes: [] }))" )? # exec_exit_code! expect_err( - Cmd.new("blablaXYZ") - |> Cmd.exec_exit_code!, - "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" + Cmd.new("blablaXYZ").exec_exit_code!(), + "Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" )? # exec_exit_code! with non-zero exit code is not an error - it returns the exit code - exit_code = + exit_code = Cmd.new("cat") - |> Cmd.arg("non_existent.txt") - |> Cmd.exec_exit_code!()? - - if exit_code == 1 then + .arg("non_existent.txt") + .exec_exit_code!()? + + if exit_code == 1 { Ok({})? - else + } else { Err(FailedExpectation( - """ - - - Expected: - 1 - - - Got: - ${Inspect.to_str(exit_code)} - - """ + \\- Expected: + \\1 + \\ + \\- Got: + \\${Str.inspect(exit_code)} ))? + } - Stdout.line!("All tests passed.")? + _ = Stdout.line!("All tests passed.") Ok({}) +} -expect_err = |err, expected_str| - if Inspect.to_str(err) == expected_str then +expect_err = |err, expected_str| { + if Str.inspect(err) == expected_str { Ok({}) - else + } else { Err(FailedExpectation( - """ - - - Expected: - ${expected_str} - - - Got: - ${Inspect.to_str(err)} - - """ - )) \ No newline at end of file + \\- Expected: + \\${expected_str} + + \\- Got: + \\${Str.inspect(err)} + )) + } +} diff --git a/tests/env.roc b/tests/env.roc deleted file mode 100644 index a5cc1b71..00000000 --- a/tests/env.roc +++ /dev/null @@ -1,75 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Stdout -import pf.Env -import pf.Path -import pf.Arg exposing [Arg] - -main! : List Arg => Result {} _ -main! = |_args| - Stdout.line!( - """ - Testing Env module functions... - - Testing Env.cwd!: - """ - )? - cwd = Env.cwd!({})? - Stdout.line!( - """ - cwd: ${Path.display(cwd)} - - Testing Env.exe_path!: - """ - )? - exe_path = Env.exe_path!({})? - Stdout.line!( - """ - exe_path: ${Path.display(exe_path)} - - Testing Env.platform!: - """ - )? - platform = Env.platform!({}) - Stdout.line!( - """ - Current platform:${Inspect.to_str(platform)} - - Testing Env.dict!: - """ - )? - env_vars = Env.dict!({}) - var_count = Dict.len(env_vars) - Stdout.line!("Environment variables count: ${Num.to_str(var_count)}")? - - some_env_vars = Dict.to_list(env_vars) |> List.take_first(3) - Stdout.line!( - """ - Sample environment variables:${Inspect.to_str(some_env_vars)} - - Testing Env.set_cwd!: - """ - )? - - # First get the current directory to restore it later - original_dir = Env.cwd!({})? - ls_list = Path.list_dir!(original_dir)? - - dir_list = - ls_list - |> List.keep_if_try!(|path| Path.is_dir!(path))? - - first_dir = - List.first(dir_list)? - - Env.set_cwd!(first_dir)? - new_cwd = Env.cwd!({})? - Stdout.line!( - """ - Changed current directory to: ${Path.display(new_cwd)} - - All tests executed. - """ - )? - - Ok({}) \ No newline at end of file diff --git a/tests/file.roc b/tests/file.roc deleted file mode 100755 index 3a95d13d..00000000 --- a/tests/file.roc +++ /dev/null @@ -1,270 +0,0 @@ -app [main!] { - pf: platform "../platform/main.roc", - json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.13.0/RqendgZw5e1RsQa3kFhgtnMP8efWoqGRsAvubx4-zus.tar.br", -} - -import pf.Stdout -import pf.Stderr -import pf.File -import pf.Arg exposing [Arg] -import pf.Cmd -import json.Json - -main! : List Arg => Result {} _ -main! = |_args| - when run_tests!({}) is - Ok(_) -> - cleanup_test_files!(FilesNeedToExist) - Err(err) -> - _ = cleanup_test_files!(FilesMaybeExist) - Err(Exit(1, "Test run failed:\n\t${Inspect.to_str(err)}")) - -run_tests! : {} => Result {} _ -run_tests! = |{}| - Stdout.line!("Testing some File functions...")? - Stdout.line!("This will create and manipulate test files in the current directory.")? - Stdout.line!("")? - - # Test basic file operations - test_basic_file_operations!({})? - - # Test file type checking - test_file_type_checking!({})? - - # Test file reader with capacity - test_file_reader_with_capacity!({})? - - # Test hard link creation - test_hard_link!({})? - - # Test file rename - test_file_rename!({})? - - # Test file exists - test_file_exists!({})? - - Stdout.line!("\nI ran all file function tests.") - -test_basic_file_operations! : {} => Result {} _ -test_basic_file_operations! = |{}| - Stdout.line!("Testing File.write_bytes! and File.read_bytes!:")? - - test_bytes = [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33] # "Hello, World!" in bytes - File.write_bytes!(test_bytes, "test_bytes.txt")? - - file_content_bytes = File.read_bytes!("test_bytes.txt")? - Stdout.line!("Bytes in test_bytes.txt: ${Inspect.to_str(file_content_bytes)}")? - - - Stdout.line!("\nTesting File.write!:")? - - File.write!({ some: "json stuff" }, "test_write.json", Json.utf8)? - json_file_content = File.read_utf8!("test_write.json")? - Stdout.line!("Content of test_write.json: ${json_file_content}")? - - Ok({}) - -test_file_type_checking! : {} => Result {} _ -test_file_type_checking! = |{}| - - Stdout.line!("\nTesting File.is_file!:")? - is_file_result = File.is_file!("test_bytes.txt")? - if is_file_result then - Stdout.line!("✓ test_bytes.txt is confirmed to be a file")? - else - Stderr.line!("✗ test_bytes.txt is not recognized as a file")? - - - Stdout.line!("\nTesting File.is_sym_link!:")? - is_symlink_one = File.is_sym_link!("test_bytes.txt")? - if is_symlink_one then - Stderr.line!("✗ test_bytes.txt is a symbolic link")? - else - Stdout.line!("✓ test_bytes.txt is not a symbolic link")? - - Cmd.exec!("ln",["-s", "test_bytes.txt","test_symlink.txt"])? - - is_symlink_two = File.is_sym_link!("test_symlink.txt")? - if is_symlink_two then - Stdout.line!("✓ test_symlink.txt is a symbolic link")? - else - Stderr.line!("✗ test_symlink.txt is not a symbolic link")? - - - Stdout.line!("\nTesting File.type!:")? - - file_type_file = File.type!("test_bytes.txt")? - Stdout.line!("test_bytes.txt file type: ${Inspect.to_str(file_type_file)}")? - - file_type_dir = File.type!(".")? - Stdout.line!(". file type: ${Inspect.to_str(file_type_dir)}")? - - file_type_symlink = File.type!("test_symlink.txt")? - Stdout.line!("test_symlink.txt file type: ${Inspect.to_str(file_type_symlink)}")? - - Ok({}) - -test_file_reader_with_capacity! : {} => Result {} _ -test_file_reader_with_capacity! = |{}| - Stdout.line!("\nTesting File.open_reader_with_capacity!:")? - - # First, create a multi-line test file - multi_line_content = "First line\nSecond line\nThird line\n" - File.write_utf8!(multi_line_content, "test_multiline.txt")? - - # Open reader with custom capacity - reader_buf_size = 3 - reader = File.open_reader_with_capacity!("test_multiline.txt", reader_buf_size)? - Stdout.line!("✓ Successfully opened reader with ${Num.to_str(reader_buf_size)} byte capacity")? - - # Read lines one by one - Stdout.line!("\nReading lines from file:")? - line1_bytes = File.read_line!(reader)? - line1_str = Str.from_utf8(line1_bytes) ? |_| LineOneInvalidUtf8 - Stdout.line!("Line 1: ${line1_str}")? - - line2_bytes = File.read_line!(reader)? - line2_str = Str.from_utf8(line2_bytes) ? |_| LineTwoInvalidUtf8 - Stdout.line!("Line 2: ${line2_str}")? - - Ok({}) - -test_hard_link! : {} => Result {} _ -test_hard_link! = |{}| - Stdout.line!("\nTesting File.hard_link!:")? - - # Create original file - File.write_utf8!("Original file content for hard link test", "test_original_file.txt")? - - # Create hard link - when File.hard_link!("test_original_file.txt", "test_link_to_original.txt") is - Ok({}) -> - Stdout.line!("✓ Successfully created hard link: test_link_to_original.txt")? - - ls_li_output = - Cmd.new("ls") - |> Cmd.args(["-li", "test_original_file.txt", "test_link_to_original.txt"]) - |> Cmd.exec_output!()? - - inodes = - Str.split_on(ls_li_output.stdout_utf8, "\n") - |> List.map(|line| - Str.split_on(line, " ") - |> List.take_first(1) - ) - - first_inode = List.get(inodes, 0) ? |_| FirstInodeNotFound - second_inode = List.get(inodes, 1) ? |_| SecondInodeNotFound - - Stdout.line!("Hard link inodes should be equal: ${Inspect.to_str(first_inode == second_inode)}")? - - # Verify both files exist and have same content - original_content = File.read_utf8!("test_original_file.txt")? - link_content = File.read_utf8!("test_link_to_original.txt")? - - if original_content == link_content then - Stdout.line!("✓ Hard link contains same content as original") - else - Stderr.line!("✗ Hard link content differs from original") - - Err(err) -> - Stderr.line!("✗ Hard link creation failed: ${Inspect.to_str(err)}") - -test_file_rename! : {} => Result {} _ -test_file_rename! = |{}| - Stdout.line!("\nTesting File.rename!:")? - - # Create original file - original_name = "test_rename_original.txt" - new_name = "test_rename_new.txt" - File.write_utf8!("Content for rename test", original_name)? - - # Rename the file - when File.rename!(original_name, new_name) is - Ok({}) -> - Stdout.line!("✓ Successfully renamed ${original_name} to ${new_name}")? - - # Verify original file no longer exists - original_exists_after = - when File.is_file!(original_name) is - Ok(exists) -> exists - Err(_) -> Bool.false - - if original_exists_after then - Stderr.line!("✗ Original file ${original_name} still exists after rename")? - else - Stdout.line!("✓ Original file ${original_name} no longer exists")? - - # Verify new file exists and has correct content - new_exists = File.is_file!(new_name)? - if new_exists then - Stdout.line!("✓ Renamed file ${new_name} exists")? - - content = File.read_utf8!(new_name)? - if content == "Content for rename test" then - Stdout.line!("✓ Renamed file has correct content")? - else - Stderr.line!("✗ Renamed file has incorrect content")? - else - Stderr.line!("✗ Renamed file ${new_name} does not exist")? - - Err(err) -> - Stderr.line!("✗ File rename failed: ${Inspect.to_str(err)}")? - - Ok({}) - -test_file_exists! : {} => Result {} _ -test_file_exists! = |{}| - Stdout.line!("\nTesting File.exists!:")? - - # Test that a file that exists returns true - filename = "test_exists.txt" - File.write_utf8!("", filename)? - - test_file_exists = File.exists!(filename) ? FileExistsCheckFailed - - if test_file_exists then - Stdout.line!("✓ File.exists! returns true for a file that exists")? - else - Stderr.line!("✗ File.exists! returned false for a file that exists")? - - # Test that a file that does not exist returns false - File.delete!(filename)? - - test_file_exists_after_delete = File.exists!(filename) ? FileExistsCheckAfterDeleteFailed - - if test_file_exists_after_delete then - Stderr.line!("✗ File.exists! returned true for a file that does not exist")? - else - Stdout.line!("✓ File.exists! returns false for a file that does not exist")? - - Ok({}) - -cleanup_test_files! : [FilesNeedToExist, FilesMaybeExist] => Result {} _ -cleanup_test_files! = |files_requirement| - Stdout.line!("\nCleaning up test files...")? - - test_files = [ - "test_bytes.txt", - "test_symlink.txt", - "test_write.json", - "test_multiline.txt", - "test_original_file.txt", - "test_link_to_original.txt", - "test_rename_new.txt", - ] - - delete_result = List.for_each_try!( - test_files, - |filename| File.delete!(filename) - ) - - when files_requirement is - FilesNeedToExist -> - delete_result ? FileDeletionFailed - - FilesMaybeExist -> - Ok({})? - - Stdout.line!("✓ Deleted all files.") - diff --git a/tests/path-test.roc b/tests/path-test.roc deleted file mode 100755 index 7011607b..00000000 --- a/tests/path-test.roc +++ /dev/null @@ -1,433 +0,0 @@ -app [main!] { - pf: platform "../platform/main.roc", - json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.13.0/RqendgZw5e1RsQa3kFhgtnMP8efWoqGRsAvubx4-zus.tar.br", -} - -import pf.Stdout -import pf.Stderr -import pf.Path -import pf.Arg exposing [Arg] -import pf.Cmd -import json.Json - -main! : List Arg => Result {} _ -main! = |_args| - when run_tests!({}) is - Ok(_) -> - cleanup_test_files!(FilesNeedToExist) - Err(err) -> - _ = cleanup_test_files!(FilesMaybeExist) - Err(Exit(1, "Test run failed:\n\t${Inspect.to_str(err)}")) - -run_tests!: {} => Result {} _ -run_tests! = |{}| - Stdout.line!( - """ - Testing Path functions... - This will create and manipulate test files and directories in the current directory. - - """ - )? - - # Test path creation - test_path_creation!({})? - - # Test file operations - test_file_operations!({})? - - # Test directory operations - test_directory_operations!({})? - - # Test hard link creation - test_hard_link!({})? - - # Test file rename - test_path_rename!({})? - - # Test path exists - test_path_exists!({})? - - Stdout.line!("\nI ran all Path function tests.") - -test_path_creation! : {} => Result {} _ -test_path_creation! = |{}| - Stdout.line!("Testing Path.from_bytes and Path.with_extension:")? - - # Test Path.from_bytes - path_bytes = [116, 101, 115, 116, 95, 112, 97, 116, 104] # "test_path" in bytes - path_from_bytes = Path.from_bytes(path_bytes) - expected_str = "test_path" - actual_str = Path.display(path_from_bytes) - - # Test Path.with_extension - base_path = Path.from_str("test_file") - path_with_ext = Path.with_extension(base_path, "txt") - - path_with_dot = Path.from_str("test_file.") - path_dot_ext = Path.with_extension(path_with_dot, "json") - - path_replace_ext = Path.from_str("test_file.old") - path_new_ext = Path.with_extension(path_replace_ext, "new") - - Stdout.line!( - """ - Created path from bytes: ${Path.display(path_from_bytes)} - Path.from_bytes result matches expected: ${Inspect.to_str(actual_str == expected_str)} - Path with extension: ${Path.display(path_with_ext)} - Extension added correctly: ${Inspect.to_str(Path.display(path_with_ext) == "test_file.txt")} - Path with dot and extension: ${Path.display(path_dot_ext)} - Extension after dot: ${Inspect.to_str(Path.display(path_dot_ext) == "test_file.json")} - Path with replaced extension: ${Path.display(path_new_ext)} - Extension replaced: ${Inspect.to_str(Path.display(path_new_ext) == "test_file.new")} - """ - )? - - Ok({}) - -test_file_operations! : {} => Result {} _ -test_file_operations! = |{}| - Stdout.line!("\nTesting Path file operations:")? - - # Test Path.write_bytes! and Path.read_bytes! - test_bytes = [72, 101, 108, 108, 111, 44, 32, 80, 97, 116, 104, 33] # "Hello, Path!" in bytes - bytes_path = Path.from_str("test_path_bytes.txt") - Path.write_bytes!(test_bytes, bytes_path)? - - # Verify file exists - _ = Cmd.exec!("test", ["-e", "test_path_bytes.txt"])? - - read_bytes = Path.read_bytes!(bytes_path)? - - Stdout.line!( - """ - Bytes written: ${Inspect.to_str(test_bytes)} - Bytes read: ${Inspect.to_str(read_bytes)} - Bytes match: ${Inspect.to_str(test_bytes == read_bytes)} - """ - )? - - # Test Path.write_utf8! and Path.read_utf8! - utf8_content = "Hello from Path module! 🚀" - utf8_path = Path.from_str("test_path_utf8.txt") - Path.write_utf8!(utf8_content, utf8_path)? - - # Check file content with cat - cat_output = Cmd.new("cat") |> Cmd.args(["test_path_utf8.txt"]) |> Cmd.exec_output!()? - - read_utf8 = Path.read_utf8!(utf8_path)? - - Stdout.line!( - """ - File content via cat: ${cat_output.stdout_utf8} - UTF-8 written: ${utf8_content} - UTF-8 read: ${read_utf8} - UTF-8 content matches: ${Inspect.to_str(utf8_content == read_utf8)} - """ - )? - - # Test Path.write! with JSON encoding - json_data = { message: "Path test", numbers: [1, 2, 3] } - json_path = Path.from_str("test_path_json.json") - Path.write!(json_data, json_path, Json.utf8)? - - json_content = Path.read_utf8!(json_path)? - - # Verify it's valid JSON by checking it contains expected fields - contains_message = Str.contains(json_content, "\"message\"") - contains_numbers = Str.contains(json_content, "\"numbers\"") - - Stdout.line!( - """ - JSON content: ${json_content} - JSON contains 'message' field: ${Inspect.to_str(contains_message)} - JSON contains 'numbers' field: ${Inspect.to_str(contains_numbers)} - """ - )? - - # Test Path.delete! - delete_path = Path.from_str("test_to_delete.txt") - Path.write_utf8!("This file will be deleted", delete_path)? - - # Verify file exists before deletion - _ = Cmd.exec!("test", ["-e", "test_to_delete.txt"])? - - Path.delete!(delete_path) ? DeleteFailed - - # Verify file is gone after deletion - exists_after_res = Cmd.exec!("test", ["-e", "test_to_delete.txt"]) - - Stdout.line!( - """ - File no longer exists: ${Inspect.to_str(Result.is_err(exists_after_res))} - """ - )? - - Ok({}) - -test_directory_operations! : {} => Result {} _ -test_directory_operations! = |{}| - Stdout.line!("\nTesting Path directory operations...")? - - # Test Path.create_dir! - single_dir = Path.from_str("test_single_dir") - Path.create_dir!(single_dir)? - - # Verify directory exists - _ = Cmd.exec!("test", ["-d", "test_single_dir"])? - - # Test Path.create_all! (nested directories) - nested_dir = Path.from_str("test_parent/test_child/test_grandchild") - Path.create_all!(nested_dir)? - - # Verify nested structure with find - find_output = Cmd.new("find") |> Cmd.args(["test_parent", "-type", "d"]) |> Cmd.exec_output!()? - - # Count directories created - dir_count = Str.split_on(find_output.stdout_utf8, "\n") |> List.len - - Stdout.line!( - """ - Nested directory structure: - ${find_output.stdout_utf8} - Number of directories created: ${Num.to_str(dir_count - 1)} - """ - )? - - # Create some files in the directory for testing - Path.write_utf8!("File 1", Path.from_str("test_single_dir/file1.txt"))? - Path.write_utf8!("File 2", Path.from_str("test_single_dir/file2.txt"))? - Path.create_dir!(Path.from_str("test_single_dir/subdir"))? - - # List directory contents - ls_contents = Cmd.new("ls") |> Cmd.args(["-la", "test_single_dir"]) |> Cmd.exec_output!()? - - Stdout.line!( - """ - Directory contents: - ${ls_contents.stdout_utf8} - """ - )? - - # Test Path.delete_empty! - empty_dir = Path.from_str("test_empty_dir") - Path.create_dir!(empty_dir)? - - # Verify it exists - _ = Cmd.exec!("test", ["-e", "test_empty_dir"])? - - Path.delete_empty!(empty_dir)? - - # Verify it's gone - exists_after_res = Cmd.exec!("test", ["-e", "test_empty_dir"]) - - Stdout.line!( - """ - Empty dir was deleted: ${Inspect.to_str(Result.is_err(exists_after_res))} - """ - )? - - # Test Path.delete_all! - # First show what we're about to delete - du_output = Cmd.new("du") |> Cmd.args(["-sh", "test_parent"]) |> Cmd.exec_output!()? - - Path.delete_all!(Path.from_str("test_parent"))? - - # Verify it's gone - parent_exists_afer_res = Cmd.exec!("test", ["-e", "test_parent"]) - - Stdout.line!( - """ - Size before delete_all: ${du_output.stdout_utf8} - Parent dir no longer exists: ${Inspect.to_str(Result.is_err(parent_exists_afer_res))} - """ - )? - - # Clean up other test directory - Path.delete_all!(single_dir)? - - Ok({}) - -get_hard_link_count! : Str => Result Str _ -get_hard_link_count! = |path_str| - ls_l = - Cmd.new("ls") - |> Cmd.args(["-l", path_str]) - |> Cmd.exec_output!()? - - hard_link_count_str = - (ls_l.stdout_utf8 - |> Str.split_on(" ") - |> List.keep_if(|str| !Str.is_empty(str)) - |> List.get(1)) ? |_| IExpectedALineWithASpaceHere(ls_l) - - Ok(hard_link_count_str) - -test_hard_link! : {} => Result {} _ -test_hard_link! = |{}| - Stdout.line!("\nTesting Path.hard_link!:")? - - # Create original file - original_path = Path.from_str("test_path_original.txt") - Path.write_utf8!("Original content for Path hard link test", original_path)? - - hard_link_count_before = get_hard_link_count!("test_path_original.txt")? - - # Create hard link - link_path = Path.from_str("test_path_hardlink.txt") - when Path.hard_link!(original_path, link_path) is - Ok({}) -> - # Get link count after - hard_link_count_after = get_hard_link_count!("test_path_original.txt")? - - # Verify both files exist and have same content - original_content = Path.read_utf8!(original_path)? - link_content = Path.read_utf8!(link_path)? - - Stdout.line!( - """ - Hard link count before: ${hard_link_count_before} - Hard link count after: ${hard_link_count_after} - Original content: ${original_content} - Link content: ${link_content} - Content matches: ${Inspect.to_str(original_content == link_content)} - """ - )? - - # Check inodes are the same - ls_li_output = - Cmd.new("ls") - |> Cmd.args(["-li", "test_path_original.txt", "test_path_hardlink.txt"]) - |> Cmd.exec_output!()? - - inodes = - Str.split_on(ls_li_output.stdout_utf8, "\n") - |> List.map(|line| - Str.split_on(line, " ") - |> List.take_first(1) - ) - - first_inode = List.get(inodes, 0) ? |_| FirstInodeNotFound - second_inode = List.get(inodes, 1) ? |_| SecondInodeNotFound - - Stdout.line!( - """ - Inode information: - ${ls_li_output.stdout_utf8} - First file inode: ${Inspect.to_str(first_inode)} - Second file inode: ${Inspect.to_str(second_inode)} - Inodes are equal: ${Inspect.to_str(first_inode == second_inode)} - """ - ) - - Err(err) -> - Stderr.line!("✗ Hard link creation failed: ${Inspect.to_str(err)}") - -test_path_rename! : {} => Result {} _ -test_path_rename! = |{}| - Stdout.line!("\nTesting Path.rename!:")? - - # Create original file - original_path = Path.from_str("test_path_rename_original.txt") - new_path = Path.from_str("test_path_rename_new.txt") - test_file_content = "Content for rename test." - - Path.write_utf8!(test_file_content, original_path) ? WriteOriginalFailed - - # Rename the file - when Path.rename!(original_path, new_path) is - Ok({}) -> - original_file_exists_after = - when Path.is_file!(original_path) is - Ok(exists) -> exists - Err(_) -> Bool.false - - if original_file_exists_after then - Stderr.line!("✗ Original file still exists after rename")? - else - Stdout.line!("✓ Original file no longer exists")? - - new_file_exists = Path.is_file!(new_path) ? NewIsFileFailed - - if new_file_exists then - Stdout.line!("✓ Renamed file exists")? - - content = Path.read_utf8!(new_path) ? NewFileReadFailed - - if content == test_file_content then - Stdout.line!("✓ Renamed file has correct content") - else - Stderr.line!("✗ Renamed file has incorrect content") - else - Stderr.line!("✗ Renamed file does not exist") - - Err(err) -> - Stderr.line!("✗ File rename failed: ${Inspect.to_str(err)}") - -test_path_exists! : {} => Result {} _ -test_path_exists! = |{}| - Stdout.line!("\nTesting Path.exists!:")? - - # Test that a file that exists returns true - filename = Path.from_str("test_path_exists.txt") - Path.write_utf8!("This file exists", filename)? - - file_exists = Path.exists!(filename) ? PathExistsCheckFailed - - if file_exists then - Stdout.line!("✓ Path.exists! returns true for a file that exists")? - else - Stderr.line!("✗ Path.exists! returned false for a file that exists")? - - # Test that a file that does not exist returns false - Path.delete!(filename)? - - file_exists_after_delete = Path.exists!(filename) ? PathExistsCheckAfterDeleteFailed - - if file_exists_after_delete then - Stderr.line!("✗ Path.exists! returned true for a file that does not exist")? - else - Stdout.line!("✓ Path.exists! returns false for a file that does not exist")? - - Ok({}) - -cleanup_test_files! : [FilesNeedToExist, FilesMaybeExist] => Result {} _ -cleanup_test_files! = |files_requirement| - Stdout.line!("\nCleaning up test files...")? - - test_files = [ - "test_path_bytes.txt", - "test_path_utf8.txt", - "test_path_json.json", - "test_path_original.txt", - "test_path_hardlink.txt", - "test_path_rename_new.txt" - ] - - # Show files before cleanup - ls_before_cleanup = Cmd.new("ls") |> Cmd.args(["-la"] |> List.concat(test_files)) |> Cmd.exec_output!()? - - Stdout.line!( - """ - Files to clean up: - ${ls_before_cleanup.stdout_utf8} - """ - )? - - delete_result = List.for_each_try!(test_files, |filename| - Path.delete!(Path.from_str(filename)) - ) - - when files_requirement is - FilesNeedToExist -> - delete_result ? FileDeletionFailed - FilesMaybeExist -> - Ok({})? - - # Verify cleanup - ls_after_cleanup_res = Cmd.exec!("ls", test_files) - - Stdout.line!( - """ - Files deleted successfully: ${Inspect.to_str(Result.is_err(ls_after_cleanup_res))} - """ - ) diff --git a/tests/sqlite.roc b/tests/sqlite.roc deleted file mode 100644 index a90240c8..00000000 --- a/tests/sqlite.roc +++ /dev/null @@ -1,179 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Env -import pf.Stdout -import pf.Sqlite -import pf.Arg exposing [Arg] - -# Run this test with: `DB_PATH=./tests/test.db roc tests/sqlite.roc` - -# Tests functions exposed by the Sqlite module that are not covered by the sqlite files in the examples folder. - -# Sql to create the table: -# CREATE TABLE test ( -# id INTEGER PRIMARY KEY AUTOINCREMENT, -# col_text TEXT NOT NULL, -# col_bytes BLOB NOT NULL, -# col_i32 INTEGER NOT NULL, -# col_i16 INTEGER NOT NULL, -# col_i8 INTEGER NOT NULL, -# col_u32 INTEGER NOT NULL, -# col_u16 INTEGER NOT NULL, -# col_u8 INTEGER NOT NULL, -# col_f64 REAL NOT NULL, -# col_f32 REAL NOT NULL, -# col_nullable_str TEXT, -# col_nullable_bytes BLOB, -# col_nullable_i64 INTEGER, -# col_nullable_i32 INTEGER, -# col_nullable_i16 INTEGER, -# col_nullable_i8 INTEGER, -# col_nullable_u64 INTEGER, -# col_nullable_u32 INTEGER, -# col_nullable_u16 INTEGER, -# col_nullable_u8 INTEGER, -# col_nullable_f64 REAL, -# col_nullable_f32 REAL -# ); - -main! : List Arg => Result {} _ -main! = |_args| - db_path = Env.var!("DB_PATH")? - - # Test Sqlite.str, Sqlite.bytes, Sqlite.i32... - - all_rows = Sqlite.query_many!({ - path: db_path, - query: "SELECT * FROM test;", - bindings: [], - # This uses the record builder syntax: https://www.roc-lang.org/examples/RecordBuilder/README.html - rows: { Sqlite.decode_record <- - col_text: Sqlite.str("col_text"), - col_bytes: Sqlite.bytes("col_bytes"), - col_i32: Sqlite.i32("col_i32"), - col_i16: Sqlite.i16("col_i16"), - col_i8: Sqlite.i8("col_i8"), - col_u32: Sqlite.u32("col_u32"), - col_u16: Sqlite.u16("col_u16"), - col_u8: Sqlite.u8("col_u8"), - col_f64: Sqlite.f64("col_f64"), - col_f32: Sqlite.f32("col_f32"), - col_nullable_str: Sqlite.nullable_str("col_nullable_str"), - col_nullable_bytes: Sqlite.nullable_bytes("col_nullable_bytes"), - col_nullable_i64: Sqlite.nullable_i64("col_nullable_i64"), - col_nullable_i32: Sqlite.nullable_i32("col_nullable_i32"), - col_nullable_i16: Sqlite.nullable_i16("col_nullable_i16"), - col_nullable_i8: Sqlite.nullable_i8("col_nullable_i8"), - col_nullable_u64: Sqlite.nullable_u64("col_nullable_u64"), - col_nullable_u32: Sqlite.nullable_u32("col_nullable_u32"), - col_nullable_u16: Sqlite.nullable_u16("col_nullable_u16"), - col_nullable_u8: Sqlite.nullable_u8("col_nullable_u8"), - col_nullable_f64: Sqlite.nullable_f64("col_nullable_f64"), - col_nullable_f32: Sqlite.nullable_f32("col_nullable_f32"), - }, - })? - - rows_texts_str = - all_rows - |> List.map(|row| Inspect.to_str(row)) - |> Str.join_with("\n") - - Stdout.line!("Rows: ${rows_texts_str}")? - - # Test query_prepared! with count - - prepared_count = Sqlite.prepare!({ - path: db_path, - query: "SELECT COUNT(*) as \"count\" FROM test;", - })? - - count = Sqlite.query_prepared!({ - stmt: prepared_count, - bindings: [], - row: Sqlite.u64("count"), - })? - - Stdout.line!("Row count: ${Num.to_str(count)}")? - - # Test execute_prepared! with different params - - prepared_update = Sqlite.prepare!({ - path: db_path, - query: "UPDATE test SET col_text = :col_text WHERE id = :id;", - })? - - Sqlite.execute_prepared!({ - stmt: prepared_update, - bindings: [ - { name: ":id", value: Integer(1) }, - { name: ":col_text", value: String("Updated text 1") }, - ], - })? - - Sqlite.execute_prepared!({ - stmt: prepared_update, - bindings: [ - { name: ":id", value: Integer(2) }, - { name: ":col_text", value: String("Updated text 2") }, - ], - })? - - # Check if the updates were successful - updated_rows = Sqlite.query_many!({ - path: db_path, - query: "SELECT COL_TEXT FROM test;", - bindings: [], - rows: Sqlite.str("col_text"), - })? - - Stdout.line!("Updated rows: ${Inspect.to_str(updated_rows)}")? - - # revert update - Sqlite.execute_prepared!({ - stmt: prepared_update, - bindings: [ - { name: ":id", value: Integer(1) }, - { name: ":col_text", value: String("example text") }, - ], - })? - - Sqlite.execute_prepared!({ - stmt: prepared_update, - bindings: [ - { name: ":id", value: Integer(2) }, - { name: ":col_text", value: String("sample text") }, - ], - })? - - # Test tagged_value - tagged_value_test = Sqlite.query_many!({ - path: db_path, - query: "SELECT * FROM test;", - bindings: [], - # This uses the record builder syntax: https://www.roc-lang.org/examples/RecordBuilder/README.html - rows: Sqlite.tagged_value("col_text"), - })? - - Stdout.line!("Tagged value test: ${Inspect.to_str(tagged_value_test)}")? - - # Let's try to trigger a `Data type mismatch` error - sql_res = Sqlite.execute!({ - path: db_path, - query: "UPDATE test SET id = :id WHERE col_text = :col_text;", - bindings: [ - { name: ":col_text", value: String("sample text") }, - { name: ":id", value: String("This should be an integer") }, - ], - }) - - when sql_res is - Ok(_) -> - crash "This should be an error." - Err(err) -> - when err is - SqliteErr(err_type, _) -> - Stdout.line!("Error: ${Sqlite.errcode_to_str(err_type)}")? - _ -> - crash "This should be an Sqlite error." - - Stdout.line!("Success!") \ No newline at end of file diff --git a/tests/tcp.roc b/tests/tcp.roc deleted file mode 100644 index ca7051de..00000000 --- a/tests/tcp.roc +++ /dev/null @@ -1,93 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Stdout -import pf.Tcp -import pf.Arg exposing [Arg] - -main! : List Arg => Result {} _ -main! = |_args| - Stdout.line!( - """ - Testing Tcp module functions... - Note: These tests require a TCP server running on localhost:8085 - You can start one with: ncat -e `which cat` -l 8085 - - """ - )? - - Stdout.line!("Testing Tcp.connect!:")? - when Tcp.connect!("127.0.0.1", 8085) is - Ok(stream) -> - Stdout.line!("✓ Successfully connected to localhost:8085")? - test_tcp_functions!(stream)? - Stdout.line!("\nAll tests executed.") - - Err(connect_err) -> - err_str = Tcp.connect_err_to_str(connect_err) - Err(Exit(1, "✗ Failed to connect: ${err_str}")) - - -test_tcp_functions! : Tcp.Stream => Result {} _ -test_tcp_functions! = |stream| - - Stdout.line!("\nTesting Tcp.write!:")? - hello_bytes = [72, 101, 108, 108, 111, 10] # "Hello\n" in bytes - Tcp.write!(stream, hello_bytes)? - - reply_msg = Tcp.read_line!(stream)? - Stdout.line!( - """ - Echo server reply: ${reply_msg} - - - Testing Tcp.write_utf8!: - """ - )? - test_message = "Test message from Roc!\n" - Tcp.write_utf8!(stream, test_message)? - - reply_msg_utf8 = Tcp.read_line!(stream)? - Stdout.line!( - """ - Echo server reply: ${reply_msg_utf8} - - - Testing Tcp.read_up_to!: - """ - )? - - do_not_read_bytes = [100, 111, 32, 110, 111, 116, 32, 114, 101, 97, 100, 32, 112, 97, 115, 116, 32, 109, 101, 65] # "do not read past meA" in bytes - Tcp.write!(stream, do_not_read_bytes)? - - nineteen_bytes = Tcp.read_up_to!(stream, 19) ? FailedReadUpTo - nineteen_bytes_as_str = Str.from_utf8(nineteen_bytes) ? ReadUpToFromUtf8 - - Stdout.line!( - """ - Tcp.read_up_to yielded: '${nineteen_bytes_as_str}' - - - Testing Tcp.read_exactly!: - """ - )? - Tcp.write_utf8!(stream, "BC")? - - three_bytes = Tcp.read_exactly!(stream, 3) ? FailedReadExactly - three_bytes_as_str = Str.from_utf8(three_bytes) ? ReadExactlyFromUtf8 - - Stdout.line!( - """ - Tcp.read_exactly yielded: '${three_bytes_as_str}' - - - Testing Tcp.read_until!: - """ - )? - Tcp.write_utf8!(stream, "Line1\nLine2\n")? - - bytes_until = Tcp.read_until!(stream, '\n') ? FailedReadUntil - bytes_until_as_str = Str.from_utf8(bytes_until) ? ReadUntilFromUtf8 - - Stdout.line!("Tcp.read_until yielded: '${bytes_until_as_str}'")? - - Ok({}) \ No newline at end of file diff --git a/tests/test.db b/tests/test.db deleted file mode 100644 index 31fa4f7e..00000000 Binary files a/tests/test.db and /dev/null differ diff --git a/tests/url.roc b/tests/url.roc deleted file mode 100644 index 185be7b3..00000000 --- a/tests/url.roc +++ /dev/null @@ -1,176 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Stdout -import pf.Url -import pf.Arg exposing [Arg] - -main! : List Arg => Result {} _ -main! = |_args| - Stdout.line!("Testing Url module functions...")? - - # Need to split this up due to high memory consumption bug - test_part_1!({})? - test_part_2!({})? - - Stdout.line!("\nAll tests executed.")? - - Ok({}) - -test_part_1! : {} => Result {} _ -test_part_1! = |{}| - # Test Url.from_str and Url.to_str - url = Url.from_str("https://example.com") - Stdout.line!("Created URL: ${Url.to_str(url)}")? - # expects "https://example.com" - - Stdout.line!("Testing Url.append:")? - - urlWithPath = Url.append(url, "some stuff") - Stdout.line!("URL with append: ${Url.to_str(urlWithPath)}")? - # expects "https://example.com/some%20stuff" - - url_search = Url.from_str("https://example.com?search=blah#fragment") - url_search_append = Url.append(url_search, "stuff") - Stdout.line!("URL with query and fragment, then appended path: ${Url.to_str(url_search_append)}")? - # expects "https://example.com/stuff?search=blah#fragment" - - url_things = Url.from_str("https://example.com/things/") - url_things_append = Url.append(url_things, "/stuff/") - url_things_append_more = Url.append(url_things_append, "/more/etc/") - Stdout.line!("URL with multiple appended paths: ${Url.to_str(url_things_append_more)}")? - # expects "https://example.com/things/stuff/more/etc/") - - # Test Url.append_param - Stdout.line!("Testing Url.append_param:")? - - url_example = Url.from_str("https://example.com") - url_example_param = Url.append_param(url_example, "email", "someone@example.com") - Stdout.line!("URL with appended param: ${Url.to_str(url_example_param)}")? - # expects "https://example.com?email=someone%40example.com" - - url_example_2 = Url.from_str("https://example.com") - url_example_2_cafe = Url.append_param(url_example_2, "café", "du Monde") - url_example_2_cafe_email = Url.append_param(url_example_2_cafe, "email", "hi@example.com") - Stdout.line!("URL with multiple appended params: ${Url.to_str(url_example_2_cafe_email)}")? - # expects "https://example.com?caf%C3%A9=du%20Monde&email=hi%40example.com")? - - # Test Url.has_query - Stdout.line!("\nTesting Url.has_query:")? - - url_with_query = Url.from_str("https://example.com?key=value#stuff") - hasQuery1 = Url.has_query(url_with_query) - Stdout.line!("URL with query has_query: ${Inspect.to_str(hasQuery1)}")? - # expects Bool.true - - url_hashtag = Url.from_str("https://example.com#stuff") - hasQuery2 = Url.has_query(url_hashtag) - Stdout.line!("URL without query has_query: ${Inspect.to_str(hasQuery2)}")? - # expects Bool.false - - Stdout.line!("\nTesting Url.has_fragment:")? - - url_key_val_hashtag = Url.from_str("https://example.com?key=value#stuff") - has_fragment = Url.has_fragment(url_key_val_hashtag) - Stdout.line!("URL with fragment has_fragment: ${Inspect.to_str(has_fragment)}")? - # expects Bool.true - - url_key_val = Url.from_str("https://example.com?key=value") - has_fragment_2 = Url.has_fragment(url_key_val) - Stdout.line!("URL without fragment has_fragment: ${Inspect.to_str(has_fragment_2)}")? - # expects Bool.false - - Stdout.line!("\nTesting Url.query:")? - - url_key_val_multi = Url.from_str("https://example.com?key1=val1&key2=val2&key3=val3#stuff") - query = Url.query(url_key_val_multi) - Stdout.line!("Query from URL: ${query}")? - # expects "key1=val1&key2=val2&key3=val3" - - url_no_query = Url.from_str("https://example.com#stuff") - query_empty = Url.query(url_no_query) - Stdout.line!("Query from URL without query: ${query_empty}") - # expects "" - -test_part_2! : {} => Result {} _ -test_part_2! = |{}| - # Test Url.fragment - Stdout.line!("\nTesting Url.fragment:")? - - url_with_fragment = Url.from_str("https://example.com#stuff") - fragment = Url.fragment(url_with_fragment) - Stdout.line!("Fragment from URL: ${fragment}")? - # expects "stuff" - - url_no_fragment = Url.from_str("https://example.com") - fragment_empty = Url.fragment(url_no_fragment) - Stdout.line!("Fragment from URL without fragment: ${fragment_empty}")? - # expects "" - - # Test Url.reserve - Stdout.line!("\nTesting Url.reserve:")? - - url_to_reserve = Url.from_str("https://example.com") - url_reserved = Url.reserve(url_to_reserve, 50) - url_with_params = url_reserved - |> Url.append("stuff") - |> Url.append_param("café", "du Monde") - |> Url.append_param("email", "hi@example.com") - - Stdout.line!("URL with reserved capacity and params: ${Url.to_str(url_with_params)}")? - # expects "https://example.com/stuff?caf%C3%A9=du%20Monde&email=hi%40example.com" - - # Test Url.with_query - Stdout.line!("\nTesting Url.with_query:")? - - url_replace_query = Url.from_str("https://example.com?key1=val1&key2=val2#stuff") - url_with_new_query = Url.with_query(url_replace_query, "newQuery=thisRightHere") - Stdout.line!("URL with replaced query: ${Url.to_str(url_with_new_query)}")? - # expects "https://example.com?newQuery=thisRightHere#stuff" - - url_remove_query = Url.from_str("https://example.com?key1=val1&key2=val2#stuff") - url_with_empty_query = Url.with_query(url_remove_query, "") - Stdout.line!("URL with removed query: ${Url.to_str(url_with_empty_query)}")? - # expects "https://example.com#stuff" - - # Test Url.with_fragment - Stdout.line!("\nTesting Url.with_fragment:")? - - url_replace_fragment = Url.from_str("https://example.com#stuff") - url_with_new_fragment = Url.with_fragment(url_replace_fragment, "things") - Stdout.line!("URL with replaced fragment: ${Url.to_str(url_with_new_fragment)}")? - # expects "https://example.com#things" - - url_add_fragment = Url.from_str("https://example.com") - url_with_added_fragment = Url.with_fragment(url_add_fragment, "things") - Stdout.line!("URL with added fragment: ${Url.to_str(url_with_added_fragment)}")? - # expects "https://example.com#things" - - url_remove_fragment = Url.from_str("https://example.com#stuff") - url_with_empty_fragment = Url.with_fragment(url_remove_fragment, "") - Stdout.line!("URL with removed fragment: ${Url.to_str(url_with_empty_fragment)}")? - # expects "https://example.com" - - # Test Url.query_params - Stdout.line!("\nTesting Url.query_params:")? - - url_with_many_params = Url.from_str("https://example.com?key1=val1&key2=val2&key3=val3") - params_dict = Url.query_params(url_with_many_params) - - # Check if params contains expected key-value pairs - Stdout.line!("params_dict: ${Inspect.to_str(params_dict)}")? - # expects Dict with key1=val1, key2=val2, key3=val3 - - # Test Url.path - Stdout.line!("\nTesting Url.path:")? - - url_with_path = Url.from_str("https://example.com/foo/bar?key1=val1&key2=val2#stuff") - path = Url.path(url_with_path) - Stdout.line!("Path from URL: ${path}")? - # expects "example.com/foo/bar" - - url_relative = Url.from_str("/foo/bar?key1=val1&key2=val2#stuff") - path_relative = Url.path(url_relative) - Stdout.line!("Path from relative URL: ${path_relative}")? - # expects "/foo/bar" - - Ok({}) diff --git a/tests/utc.roc b/tests/utc.roc deleted file mode 100644 index 7c2fa186..00000000 --- a/tests/utc.roc +++ /dev/null @@ -1,93 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Stdout -import pf.Utc -import pf.Sleep -import pf.Arg exposing [Arg] - -main! : List Arg => Result {} _ -main! = |_args| - # Test basic time operations - test_time_conversion!({})? - - # Test time delta operations - test_time_delta!({})? - - Stdout.line!("\nAll tests executed.") - -test_time_conversion! : {} => Result {} _ -test_time_conversion! = |{}| - # Get current time - now = Utc.now!({}) - - millis_since_epoch = Utc.to_millis_since_epoch(now) - Stdout.line!("Current time in milliseconds since epoch: ${Num.to_str(millis_since_epoch)}")? - - # Basic sanity: should be non-negative - err_on_false(millis_since_epoch >= 0)? - - time_from_millis = Utc.from_millis_since_epoch(millis_since_epoch) - Stdout.line!("Time reconstructed from milliseconds: ${Utc.to_iso_8601(time_from_millis)}")? - - # Verify exact round-trip via ISO strings - err_on_false(Utc.to_iso_8601(time_from_millis) == Utc.to_iso_8601(now))? - - nanos_since_epoch = Utc.to_nanos_since_epoch(now) - Stdout.line!("Current time in nanoseconds since epoch: ${Num.to_str(nanos_since_epoch)}")? - - # Sanity: also non-negative and ≥ millis * 1_000_000 - err_on_false(nanos_since_epoch >= 0)? - err_on_false(Num.to_frac(nanos_since_epoch) >= Num.to_frac(millis_since_epoch) * 1_000_000)? - - time_from_nanos = Utc.from_nanos_since_epoch(nanos_since_epoch) - Stdout.line!("Time reconstructed from nanoseconds: ${Utc.to_iso_8601(time_from_nanos)}")? - - # Verify exact round-trip - err_on_false(Utc.to_iso_8601(time_from_nanos) == Utc.to_iso_8601(now))? - - Ok({}) - -test_time_delta! : {} => Result {} _ -test_time_delta! = |{}| - Stdout.line!("\nTime delta demonstration:")? - - start = Utc.now!({}) - Stdout.line!("Starting time: ${Utc.to_iso_8601(start)}")? - - Sleep.millis!(1500) - - finish = Utc.now!({}) - Stdout.line!("Ending time: ${Utc.to_iso_8601(finish)}")? - - # start should be before finish - err_on_false(Utc.to_millis_since_epoch(finish) > Utc.to_millis_since_epoch(start))? - - delta_millis = Utc.delta_as_millis(start, finish) - Stdout.line!("Time elapsed: ${Num.to_str(delta_millis)} milliseconds")? - - # For comparison, also show delta in nanoseconds - delta_nanos = Utc.delta_as_nanos(start, finish) - Stdout.line!("Time elapsed: ${Num.to_str(delta_nanos)} nanoseconds")? - - # Verify both deltas are positive and proportional - err_on_false(delta_millis > 0)? - err_on_false(delta_nanos > 0)? - err_on_false(Num.to_frac(delta_nanos) >= Num.to_frac(delta_millis) * 1_000_000)? - - # Verify conversion: nanoseconds to milliseconds - calculated_millis = Num.to_frac(delta_nanos) / 1_000_000 - Stdout.line!("Nanoseconds converted to milliseconds: ${Num.to_str(calculated_millis)}")? - - # Check that deltaNanos / 1_000_000 is approximately equal to deltaMillis - difference = Num.abs(calculated_millis - Num.to_frac(delta_millis)) - err_on_false(difference < 1)? - - Stdout.line!("Verified: deltaMillis and deltaNanos/1_000_000 match within tolerance")? - - Ok({}) - -err_on_false = |bool| - if bool then - Ok({}) - else - Err(StrErr("A Test failed.")) \ No newline at end of file