diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3ea1498..042a2b7e 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,54 @@ 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 + + - name: Install Roc nightly + run: ./ci/install_roc.sh + env: + GITHUB_TOKEN: ${{ github.token }} - - run: roc version + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl - - 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 index c72bec1b..d36db27f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -5,109 +5,113 @@ on: branches: - main paths: - - '**.roc' - release: + - ".github/workflows/deploy-docs.yml" + - "ci/install_roc.sh" + - "docs/**" + - "platform/**" + workflow_run: + workflows: + - Release types: - - created - + - completed workflow_dispatch: -# this cancels workflows currently in progress if you start a new one concurrency: - group: "pages" + group: pages cancel-in-progress: true -# Do not add permissions here! Configure them at the job level! permissions: contents: read jobs: deploy: + name: Deploy docs + if: github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'workflow_dispatch') + runs-on: ubuntu-24.04 environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-24.04 permissions: + contents: read 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 + - name: Install Roc nightly + run: ./ci/install_roc.sh + env: + GITHUB_TOKEN: ${{ github.token }} - - run: roc version - - - name: Create temp directory for docs + - name: Create Pages tree run: mkdir -p ./temp_docs - - - name: Download and extract docs for each release + + - name: Download release docs + env: + GH_TOKEN: ${{ github.token }} 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 + set -euo pipefail + + releases_json="$(gh api "repos/${{ github.repository }}/releases?per_page=100")" + + latest_release="$(printf '%s' "$releases_json" | jq -r '[.[] | select(.draft == false and .prerelease == false)][0].tag_name // empty')" + if [ -z "$latest_release" ]; then + latest_release="$(printf '%s' "$releases_json" | jq -r '[.[] | select(.draft == false)][0].tag_name // empty')" + fi + if [ -z "$latest_release" ]; then + latest_release="main" + fi + + printf '%s' "$releases_json" | jq -r '.[] | select(.draft == false) | .tag_name' | while IFS= read -r release_name; do + [ -n "$release_name" ] || continue + + download_dir="./temp_docs/_downloads/$release_name" + mkdir -p "$download_dir" + + if gh release download "$release_name" --repo "${{ github.repository }}" --pattern docs.tar.gz --dir "$download_dir" --clobber; then + mkdir -p "./temp_docs/$release_name" + tar -xzf "$download_dir/docs.tar.gz" -C "./temp_docs/$release_name" --strip-components=1 + echo "Added docs for release $release_name" else - echo "Error: docs.tar.gz not found for release ${release_name}" + echo "No docs.tar.gz asset found for release $release_name; skipping" 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') - + rm -rf ./temp_docs/_downloads + 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}" + sed "s/LATESTVERSION/${latest_release}/g" ./docs/index.html > ./temp_docs/index.html + echo "Created root redirect to $latest_release" else - echo "Error: index.html not found in docs folder" + echo "Error: docs/index.html not found" exit 1 fi - - name: Add docs for main branch + - name: Generate main branch docs env: ROC_DOCS_URL_ROOT: /basic-cli/main run: | - roc docs ./platform/main.roc + set -euo pipefail + + roc docs --output=generated-docs docs/basic-cli.roc + mkdir -p ./temp_docs/main + cp -R ./generated-docs/. ./temp_docs/main/ - mkdir -p "./temp_docs/main" + - name: Fix legacy generated URLs + run: | + set -euo pipefail - mv ./generated-docs/* ./temp_docs/main - + find ./temp_docs -type f \( -name "*.html" -o -name "*.js" \) -exec sed -i 's|/packages/basic-cli/|/basic-cli/|g' {} + - - name: Upload artifact + - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: - # Upload the processed docs folder - path: "./temp_docs" - + 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..8a4b2618 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,250 @@ +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: Install Roc nightly + run: ./ci/install_roc.sh + env: + GITHUB_TOKEN: ${{ github.token }} + + - 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: Install Roc nightly + run: ./ci/install_roc.sh + env: + GITHUB_TOKEN: ${{ github.token }} + + - 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" + + build-docs: + name: Build Docs + runs-on: ubuntu-24.04 + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Roc nightly + run: ./ci/install_roc.sh + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Generate docs archive + env: + DOCS_URL_ROOT: ${{ github.event_name == 'workflow_dispatch' && format('/basic-cli/{0}', inputs.release_tag) || '/basic-cli/pull-request' }} + run: | + set -euo pipefail + + ROC_DOCS_URL_ROOT="$DOCS_URL_ROOT" roc docs --output=generated-docs docs/basic-cli.roc + tar -czf docs.tar.gz generated-docs + + - name: Upload docs artifact + uses: actions/upload-artifact@v4 + with: + name: platform-docs + path: docs.tar.gz + retention-days: 30 + + create-release: + name: Create GitHub Release + needs: [build-and-bundle, test-bundle, build-docs] + 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: Download docs artifact + uses: actions/download-artifact@v4 + with: + name: platform-docs + + - 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 + + if [ ! -f docs.tar.gz ]; then + echo "Error: docs.tar.gz not found" + exit 1 + fi + + # Create release with auto-generated notes + gh release create "${{ github.event.inputs.release_tag }}" \ + "$BUNDLE_FILE" \ + docs.tar.gz \ + --title "${{ github.event.inputs.release_tag }}" \ + --generate-notes \ + --notes "Platform bundle and docs built with Rust (stable) and the latest Roc nightly (new compiler)." + + echo "Release created: ${{ github.event.inputs.release_tag }}" + echo "Bundle uploaded: $BUNDLE_FILE" + echo "Docs uploaded: docs.tar.gz" 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..977f6ba8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,18 @@ +### 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 target generated-docs +generated-docs-* .direnv .envrc *.rs.bk @@ -28,6 +38,7 @@ vgcore.* #editors .idea/ .vscode/ +.claude/ .ignore .exrc .vimrc @@ -47,17 +58,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 +75,20 @@ 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 + +# Roc nightly downloaded by ci/install_roc.sh +.roc-bin/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23d78af0..5581a25f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,101 @@ # Contributing +Thanks for helping improve `basic-cli`. + +CI uses the current Roc nightly from [`roc-lang/nightlies`](https://github.com/roc-lang/nightlies). +For local work, use any recent `roc` on `PATH`, or run `./ci/install_roc.sh` +to install the same latest nightly style used by CI. + ## Code of Conduct -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. +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. + +## Version Requirements + +Check the compiler available locally: + +```sh +roc version +``` + +To install the latest nightly locally: + +```sh +./ci/install_roc.sh +ROC_BIN_DIR="$(dirname "$(find .roc-bin -type f -name roc | head -1)")" +export PATH="$(pwd)/$ROC_BIN_DIR:$PATH" +``` + +## Updating Roc Glue + +CI intentionally tracks the current nightly, so compiler updates are adopted as +soon as a new nightly is published. If a nightly changes the host ABI: + +1. Run `./ci/regenerate_glue.sh` to refresh `src/roc_platform_abi.rs`. +2. Reconcile `src/lib.rs` if generated names or layouts changed. +3. Run `cargo check` and `./ci/all_tests.sh`. + +## Verification + +Run the full local check before opening release or CI-facing changes: + +```sh +./ci/all_tests.sh +``` + +The script builds the host, checks and builds every example, checks test apps, 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. + +For faster local iterations when the platform host is already built: + +```sh +NO_BUILD=1 NO_BUNDLE=1 ./ci/all_tests.sh +``` + +## Rust Glue + +The Rust host ABI is generated from `platform/main.roc` using Roc's `RustGlue.roc` generator: + +```sh +./ci/regenerate_glue.sh +./ci/regenerate_glue.sh --check +``` + +Commit `src/roc_platform_abi.rs` with any platform API change and the matching Rust host updates. + +The script defaults to a sibling `../roc` checkout. Override paths when needed: + +```sh +ROC=../roc/zig-out/bin/roc ROC_SRC=../roc ./ci/regenerate_glue.sh +``` + +Use `ci/regenerate_glue.sh --check` separately when reviewing platform ABI changes; CI intentionally treats the committed Rust glue as the host ABI source of truth. -## How to generate docs? +Do not edit generated glue by hand. + +## Examples + +Every checked-in example should pass `roc check`, `roc test`, and `roc build` +with the current nightly. + +Examples should include a top-level `main!` annotation. When the full platform error row would distract from the example, map low-level errors into a small example-domain error or use `_` for the error type. Prefer postfix `?`, infix `?`, or `??` for effect results instead of ignoring them. + +HTTP examples use Roc's builtin `Json` parser directly through `Http.get!`. + +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 current nightly. + +## Documentation + +Generate platform docs from the docs entrypoint: + +```sh +ROC_DOCS_URL_ROOT=/basic-cli/main roc docs --output=generated-docs docs/basic-cli.roc +``` -You can generate the documentation locally and then start a web server to host your files. +To preview generated docs locally: -```bash -roc docs platform/main.roc +```sh cd generated-docs -simple-http-server --nocache --index # comes pre-installed if you use `nix develop`, otherwise use `cargo install simple-http-server`. +simple-http-server --nocache --index ``` -Open http://0.0.0.0:8000 in your browser +CI attaches `docs.tar.gz` to each GitHub Release and deploys release folders plus the current `main` docs to GitHub Pages. diff --git a/Cargo.lock b/Cargo.lock index d5cdcf99..ca55744e 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" @@ -75,22 +69,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "crossterm" version = "0.29.0" @@ -155,7 +133,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 +159,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 +189,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 +198,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" @@ -310,11 +267,11 @@ dependencies = [ "hyper", "hyper-util", "rustls", - "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -345,9 +302,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 +350,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 +367,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.59.0", ] @@ -438,12 +386,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "parking_lot" version = "0.12.5" @@ -503,12 +445,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 +462,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 +501,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -698,18 +518,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -721,53 +529,21 @@ 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", "untrusted", ] -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "shlex" version = "1.3.0" @@ -820,12 +596,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 +622,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" @@ -957,21 +717,12 @@ 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" +name = "webpki-roots" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ - "wit-bindgen", + "rustls-pki-types", ] [[package]] @@ -1020,15 +771,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -1093,12 +835,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..28a7ae75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,66 +1,34 @@ -[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). `ring` is the rustls crypto +# backend, and WebPKI roots keep the static host self-contained for Roc linking. +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", "webpki-tokio", "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..2912ff2b 100644 --- a/README.md +++ b/README.md @@ -5,29 +5,27 @@ # 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,... - -: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/) - -## Running Locally - -If you clone this repo instead of using the release URL you'll need to build the platform once: -```sh -./jump-start.sh -roc build.roc --linker=legacy -``` -Then you can run like usual: -```sh -$ roc examples/hello-world.roc -Hello, World! -``` +A Roc [platform](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform) for command-line programs. + +`basic-cli` supports command execution, directories, environment variables, files, HTTP, locales, paths, random seeds, sleeping, SQLite, standard input/output/error, TCP, terminal raw mode, and UTC time. + +## Examples + +The [examples](examples/) directory shows small CLI programs for common tasks like reading files, running commands, making HTTP requests, working with SQLite, and reading stdin. + +If you want to run an example without building `basic-cli` from source, use a released bundle URL in place of `"../platform/main.roc"`. To run examples from a local checkout, build the platform first; see [CONTRIBUTING.md](CONTRIBUTING.md). + +HTTP examples use Roc's builtin `Json` parser directly through `Http.get!`. + +## Documentation + +- [latest release](https://roc-lang.github.io/basic-cli/) +- [latest main branch](https://roc-lang.github.io/basic-cli/main/) + +## Help + +Ask questions on [Roc Zulip](https://roc.zulipchat.com), especially in the `#beginners` stream. + +## Contributing + +Contributor setup, verification, generated glue, and documentation publishing notes live in [CONTRIBUTING.md](CONTRIBUTING.md). 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/TODO.md b/TODO.md new file mode 100644 index 00000000..ec31721e --- /dev/null +++ b/TODO.md @@ -0,0 +1,20 @@ +# TODO + +## Release Readiness + +- Get all release-facing workflows green. + - `.github/workflows/ci.yml` + - `.github/workflows/release.yml` on pull requests + - Bundle tests on macOS ARM64, macOS x64, Linux x64, and Linux ARM64 + - Docs generation and `docs.tar.gz` upload + +- Publish the release only after the manual Release workflow succeeds for the intended tag. + - Confirm the platform bundle asset is attached. + - Confirm `docs.tar.gz` is attached. + - Confirm GitHub Pages deploys release docs and updates the latest-release redirect. + +## API Backlog + +- Switch `Path.type!` to receiver-style effect calls when Roc supports it. + - Tracked upstream: https://github.com/roc-lang/roc/issues/9864 + - Desired app-facing shape: `path.type!()`. 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..3c7bbc0d --- /dev/null +++ b/build.sh @@ -0,0 +1,144 @@ +#!/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) + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Configure cc-rs (used by the bundled libsqlite3-sys) to cross-compile C to musl +# with `zig cc`, so neither CI nor local builds need a musl-gcc cross toolchain. +# Only applied for *-linux-musl targets, and only when zig is available; if zig +# is absent we leave the environment untouched so a real musl cross toolchain can +# be used instead. +configure_musl_cc() { + local rust_triple=$1 + local zig_target + case "$rust_triple" in + x86_64-unknown-linux-musl) zig_target="x86_64-linux-musl" ;; + aarch64-unknown-linux-musl) zig_target="aarch64-linux-musl" ;; + *) return 0 ;; + esac + command -v zig >/dev/null 2>&1 || return 0 + + local key="${rust_triple//-/_}" + export ZIG_CC_TARGET="$zig_target" + export "CC_${key}=${ROOT_DIR}/ci/zig-cc.sh" + export "AR_${key}=${ROOT_DIR}/ci/zig-ar.sh" + export "CFLAGS_${key}=-Wno-error" + echo " (using zig cc for $rust_triple)" +} + +# 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)..." + configure_musl_cc "$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 + configure_musl_cc "$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/" + 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..bdca37d2 100755 --- a/ci/all_tests.sh +++ b/ci/all_tests.sh @@ -1,192 +1,250 @@ #!/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 +# Cleanup function to restore examples and stop HTTP server +cleanup() { + echo "" + echo "=== Cleaning up ===" - exit 1 -fi + # Restore examples from backups + for f in examples/*.roc.bak; do + if [ -f "$f" ]; then + mv "$f" "${f%.bak}" + fi + done -TESTS_DIR="$(dirname "$EXAMPLES_DIR")/tests/" -export TESTS_DIR + # Stop HTTP server if running + if [ -n "${HTTP_SERVER_PID:-}" ]; then + kill "$HTTP_SERVER_PID" 2>/dev/null || true + fi -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 + # Remove built binaries + for example in "${EXAMPLE_NAMES[@]}"; do + rm -f "examples/${example}" + done - # build the basic-cli platform - $ROC dev ./build.roc --linker=legacy -- --roc $ROC -fi + # Remove bundle file + if [ -n "${BUNDLE_FILE:-}" ] && [ -f "$BUNDLE_FILE" ]; then + rm -f "$BUNDLE_FILE" + 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 +# Set up trap to ensure cleanup runs on exit +trap cleanup EXIT -$ROC ci/check_all_exposed_funs_tested.roc -$ROC ci/check_cargo_versions_match.roc +echo "=== basic-cli CI ===" +echo "" -# roc build -architecture=$(uname -m) +# Roc is provided on PATH by ci/install_roc.sh in CI (the current nightly from +# roc-lang/nightlies). For local runs any `roc` on PATH is used. +if ! command -v roc &>/dev/null; then + echo "Error: 'roc' was not found on PATH." >&2 + echo "Install it with ./ci/install_roc.sh or put a recent roc binary on PATH." >&2 + exit 1 +fi -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") +echo "Using roc version: $(roc version)" - if [ "$base_file" == "temp-dir.roc" ]; then - $ROC build $roc_file $ROC_BUILD_FLAGS --linker=legacy - else - $ROC build $roc_file $ROC_BUILD_FLAGS +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 -done -for roc_file in $TESTS_DIR*.roc; do - $ROC build $roc_file $ROC_BUILD_FLAGS -done +# NOTE: the committed src/roc_platform_abi.rs (generated glue) is the source of +# truth for the host ABI and is intentionally NOT re-checked here: CI installs +# only the roc binary (no glue spec to regenerate from). Correctness is enforced +# by the host compiling against committed glue and the end-to-end expect tests +# below exercising the real ABI. If a new nightly changes generated glue, run +# ci/regenerate_glue.sh locally, reconcile src/lib.rs, and commit together. + +# Build the platform +if [ "${NO_BUILD:-}" != "1" ]; then + echo "" + echo "=== Building platform ===" + ./build.sh +else + echo "" + echo "=== Skipping platform build (NO_BUILD=1) ===" +fi -# 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 - fi -done +EXAMPLES_DIR="${ROOT_DIR}/examples/" +export EXAMPLES_DIR -# prep for next step -cd ci/rust_http_server -cargo build --release -cd ../.. +TESTS_DIR="${ROOT_DIR}/tests/" +export TESTS_DIR -# check output with linux expect -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") +# 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" + "file-read-buffered" + "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 - # Skip env-var.roc when on aarch64 - if [ "$architecture" == "aarch64" ] && [ "$base_file" == "env-var.roc" ]; then - continue +# 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 - ## 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 +# 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 + else + 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 - roc_file_only="$(basename "$roc_file")" - no_ext_name=${roc_file_only%.*} - - # 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 - - expect ci/expect_scripts/$no_ext_name.exp +echo "" +echo "=== Checking examples ===" +for example in "${EXAMPLE_NAMES[@]}"; do + echo "Checking: ${example}.roc" + roc check "examples/${example}.roc" done -for roc_file in $TESTS_DIR*.roc; do - roc_file_only="$(basename "$roc_file")" - no_ext_name=${roc_file_only%.*} - - expect ci/expect_scripts/$no_ext_name.exp +echo "" +echo "=== Testing examples ===" +for example in "${EXAMPLE_NAMES[@]}"; do + echo "Testing: ${example}.roc" + roc test "examples/${example}.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") +TESTS_FILES=() +for roc_file in "${TESTS_DIR}"*.roc; do + [ -f "$roc_file" ] && TESTS_FILES+=("$(basename "${roc_file%.roc}")") +done -# roc dev (some expects only run with `roc dev`) -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") +echo "" +echo "=== Checking tests ===" +for test in "${TESTS_FILES[@]}"; do + echo "Checking: ${test}.roc" + roc check "tests/${test}.roc" +done +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 - # 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 +# 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 - # 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 +# 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 - else - - $ROC dev $roc_file $ROC_BUILD_FLAGS 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 - fi - done - - if [ "$base_file" == "sqlite.roc" ]; then - DB_PATH=${TESTS_DIR}test.db $ROC dev $roc_file $ROC_BUILD_FLAGS + set +e + expect "ci/expect_scripts/${example}.exp" + EXIT_CODE=$? + set -e + if [ $EXIT_CODE -eq 0 ]; then + echo "PASS: $example" else - $ROC dev $roc_file $ROC_BUILD_FLAGS + echo "FAIL: $example (exit code: $EXIT_CODE)" + FAILED=1 fi done -# remove Dir example directorys if they exist -rm -rf dirExampleE -rm -rf dirExampleA -rm -rf dirExampleD - -# `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 - - if grep -qE '^\s*expect(\s+|$)' "$file"; then - - # don't exit script if test_command fails - set +e - test_command=$($ROC test "$file") - test_exit_code=$? - set -e - - if [[ $test_exit_code -ne 0 && $test_exit_code -ne 2 ]]; then - exit $test_exit_code - fi - fi +echo "" +if [ $FAILED -eq 0 ]; then + if [ "$USE_BUNDLE" = true ]; then + echo "=== All tests passed (with bundle)! ===" + else + echo "=== All tests passed! ===" fi -done - -# test building website -$ROC docs platform/main.roc +else + echo "=== Some tests failed ===" + exit 1 +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 index ab83a72e..8016a695 100644 --- a/ci/expect_scripts/file-read-buffered.exp +++ b/ci/expect_scripts/file-read-buffered.exp @@ -9,7 +9,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 -re "Done reading file: \\{ bytes_read: \[0-9\]+, lines_read: \[0-9\]+ \\}\r\n" { expect eof { check_exit_and_segfault } 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.exp b/ci/expect_scripts/http-client.exp similarity index 54% rename from ci/expect_scripts/http.exp rename to ci/expect_scripts/http-client.exp index d5cc2f0a..a9755786 100644 --- a/ci/expect_scripts/http.exp +++ b/ci/expect_scripts/http-client.exp @@ -3,26 +3,21 @@ # uncomment line below for debugging # exp_internal 1 -set timeout 7 +set timeout 15 -source ./ci/expect_scripts/shared-code.exp - -# Start server to test with in the background and capture its process ID +# 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 3 +sleep 2 -spawn $env(EXAMPLES_DIR)http +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" { - - # we can kill our rust server now - exec kill $server_pid - - expect "\r\n" { - expect "\r\n" { - expect eof { - check_exit_and_segfault + expect "send! returned status 200.\r\n" { + expect "invalid JSON was rejected.\r\n" { + expect "invalid UTF-8 was rejected.\r\n" { + exec kill $server_pid + exit 0 } } } @@ -32,4 +27,4 @@ expect "I received 'Hello utf8' from the server.\r\n" { 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 +exit 1 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..13fc907b 100644 --- a/ci/expect_scripts/path.exp +++ b/ci/expect_scripts/path.exp @@ -11,10 +11,10 @@ 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) +display: path.roc }] 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/install_roc.sh b/ci/install_roc.sh new file mode 100755 index 00000000..d91269ed --- /dev/null +++ b/ci/install_roc.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Install the current Roc nightly from roc-lang/nightlies. +# +# CI intentionally tracks the latest nightly compiler. The committed Rust glue is +# still the host ABI source of truth; if a new nightly changes generated glue, +# regenerate src/roc_platform_abi.rs and reconcile src/lib.rs in the same change. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) PLATFORM="linux_x86_64" ;; + Linux-aarch64|Linux-arm64) PLATFORM="linux_arm64" ;; + Darwin-x86_64) PLATFORM="macos_x86_64" ;; + Darwin-arm64) PLATFORM="macos_apple_silicon" ;; + *) echo "install_roc: unsupported platform $(uname -s)-$(uname -m)" >&2; exit 1 ;; +esac + +# Authenticated requests avoid GitHub's low anonymous API rate limit in CI. +AUTH=() +if [ -n "${GITHUB_TOKEN:-}" ]; then + AUTH=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi +API="https://api.github.com/repos/roc-lang/nightlies/releases" + +echo "install_roc: resolving latest roc-lang/nightlies release..." +TAG="$(curl -fsSL "${AUTH[@]}" "${API}/latest" \ + | grep -oE "\"tag_name\": *\"nightly-[^\"]+\"" \ + | head -1 | sed -E 's/.*"(nightly-[^"]*)".*/\1/' || true)" + +if [ -z "${TAG:-}" ]; then + echo "install_roc: could not resolve latest roc-lang/nightlies release." >&2 + exit 1 +fi + +URL="$(curl -fsSL "${AUTH[@]}" "${API}/tags/${TAG}" \ + | grep -oE "https://github.com/roc-lang/nightlies/releases/download/${TAG}/roc_nightly-${PLATFORM}-[^\"]*\.tar\.gz" \ + | head -1 || true)" + +if [ -z "${URL:-}" ]; then + echo "install_roc: release ${TAG} has no ${PLATFORM} asset." >&2 + exit 1 +fi + +DEST="$ROOT_DIR/.roc-bin" +rm -rf "$DEST" && mkdir -p "$DEST" +echo "install_roc: downloading ${URL}" +curl -fsSL "$URL" | tar -xz -C "$DEST" + +BIN_DIR="$(dirname "$(find "$DEST" -type f -name roc | head -1)")" +if [ -z "${BIN_DIR:-}" ]; then + echo "install_roc: roc binary not found in downloaded archive." >&2 + exit 1 +fi + +export PATH="$BIN_DIR:$PATH" +if [ -n "${GITHUB_PATH:-}" ]; then + echo "$BIN_DIR" >> "$GITHUB_PATH" +fi + +echo "install_roc: installed $(roc version)" 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/rust_http_server/src/main.rs b/ci/rust_http_server/src/main.rs index 4a9bbd64..eb931323 100644 --- a/ci/rust_http_server/src/main.rs +++ b/ci/rust_http_server/src/main.rs @@ -32,9 +32,20 @@ async fn handle_request(req: Request) -> Result, Gen .header("Content-Type", "text/plain; charset=utf-8") .body(full(Bytes::from(utf8_bytes)))? }, + (&Method::GET, "/invalid-json") => { + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(full(Bytes::from_static(b"not-json")))? + }, + (&Method::GET, "/invalid-utf8") => { + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/octet-stream") + .body(full(Bytes::from_static(&[0xff])))? + }, _ => { - // Default response (original functionality) - // output of: Encode.to_bytes({foo: "Hello Json!"}, Json.utf8) + // Default JSON response for `{ foo: "Hello Json!" }`. let json_bytes: Vec = vec![123, 34, 102, 111, 111, 34, 58, 34, 72, 101, 108, 108, 111, 32, 74, 115, 111, 110, 33, 34, 125]; Response::builder() 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/ci/zig-ar.sh b/ci/zig-ar.sh new file mode 100755 index 00000000..f47a6703 --- /dev/null +++ b/ci/zig-ar.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Archiver shim companion to zig-cc.sh — lets cc-rs create static archives with +# `zig ar` when cross-compiling C to musl. build.sh points AR_ here. +set -euo pipefail +ZIG_BIN="${ZIG:-zig}" +exec "$ZIG_BIN" ar "$@" diff --git a/ci/zig-cc.sh b/ci/zig-cc.sh new file mode 100755 index 00000000..9abe621e --- /dev/null +++ b/ci/zig-cc.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# C-compiler shim so cc-rs (used by the bundled libsqlite3-sys) can cross-compile +# C to musl using `zig cc`, avoiding the need for a musl-gcc cross toolchain in +# CI. build.sh points CC_ at this script for *-linux-musl targets and +# sets ZIG_CC_TARGET to the zig target triple (e.g. x86_64-linux-musl). +# +# cc-rs passes the *rust* triple via `--target=...`; zig wants its own triple, so +# we strip the incoming target flags and substitute ZIG_CC_TARGET. +set -euo pipefail + +: "${ZIG_CC_TARGET:?ZIG_CC_TARGET must be set (e.g. x86_64-linux-musl)}" +ZIG_BIN="${ZIG:-zig}" + +args=() +skip_next=false +for a in "$@"; do + if [ "$skip_next" = true ]; then + skip_next=false + continue + fi + case "$a" in + --target=*) continue ;; + -target) skip_next=true; continue ;; + x86_64-unknown-linux-musl|aarch64-unknown-linux-musl) continue ;; + *) args+=("$a") ;; + esac +done + +exec "$ZIG_BIN" cc -target "$ZIG_CC_TARGET" "${args[@]}" 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/docs/basic-cli.roc b/docs/basic-cli.roc new file mode 100644 index 00000000..d9e0e92f --- /dev/null +++ b/docs/basic-cli.roc @@ -0,0 +1,10 @@ +app [main!] { pf: platform "../platform/main.roc" } + +import pf.Stdout + +## Documentation entrypoint for the `basic-cli` platform. +main! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + Stdout.line!("basic-cli documentation entrypoint") ? |_| Exit(1) + Ok({}) +} diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 2b8e998c..00000000 --- a/examples/README.md +++ /dev/null @@ -1,10 +0,0 @@ - -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`. -> - 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). - -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..8553741e 100644 --- a/examples/bytes-stdin-stdout.roc +++ b/examples/bytes-stdin-stdout.roc @@ -3,13 +3,13 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdin import pf.Stdout import pf.Stderr -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder -main! : List Arg => Result {} _ -main! = |_args| - data = Stdin.bytes!({})? +main! : List(Str) => Try({}, _) +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..77a82e28 100644 --- a/examples/command-line-args.roc +++ b/examples/command-line-args.roc @@ -1,37 +1,18 @@ -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! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |args| { + # Skip first arg (executable path), get the remaining args + match args.drop_first(1) { + [first_arg, ..] => { + Stdout.line!("received argument: ${first_arg}") ? |_| Exit(1) + Ok({}) + } + [] => { + Stdout.line!("Error: I expected one argument, but got none.") ? |_| Exit(1) + Err(Exit(1)) + } + } +} diff --git a/examples/command.roc b/examples/command.roc index 1aaa362b..d7632dc4 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -2,50 +2,54 @@ 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! : List Arg => Result {} _ +main! : List(Str) => Try({}, [Exit(I32), ..]) main! = |_args| - - # Simplest way to execute a command (prints to your terminal). - Cmd.exec!("echo", ["Hello"])? - - # To execute and capture the output (stdout and stderr) without inheriting your terminal. - cmd_output = - Cmd.new("echo") - |> Cmd.args(["Hi"]) - |> Cmd.exec_output!()? - - Stdout.line!("${Inspect.to_str(cmd_output)}")? - - # 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!()? - - # 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!()? - - Stdout.line!("Exit code: ${Num.to_str(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") - |> Cmd.args(["Hi"]) - |> Cmd.exec_output_bytes!()? - - Stdout.line!("${Inspect.to_str(cmd_output_bytes)}")? - - Ok({}) + match run!() { + Ok(_) => Ok({}) + Err(CommandFailed(message)) => { + Stdout.line!("Command example failed: ${message}") ? |_| Exit(1) + Err(Exit(1)) + } + } + +run! : () => Try({}, [CommandFailed(Str)]) +run! = || { + # Simplest way to execute a command (prints to your terminal). + Cmd.exec!("echo", ["Hello"]) ? |_| CommandFailed("exec echo failed") + + # To execute and capture the output (stdout and stderr) without inheriting your terminal. + cmd_output = + Cmd.new("echo") + .args(["Hi"]) + .exec_output!() ? |_| CommandFailed("exec_output echo failed") + + Stdout.line!("${Str.inspect(cmd_output)}") ? |_| CommandFailed("stdout write failed") + + # To run a command with a controlled environment. + Cmd.new("/usr/bin/env") + .args(["-i", "BAZ=DUCK", "FOO=BAR", "XYZ=ABC"]) + .exec_cmd!() ? |_| CommandFailed("exec_cmd env failed") + + # 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!() ? |_| CommandFailed("exec_exit_code cat failed") + + Stdout.line!("Exit code: ${exit_code.to_str()}") ? |_| CommandFailed("stdout write failed") + + # 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!() ? |_| CommandFailed("exec_output_bytes echo failed") + + Stdout.line!("${Str.inspect(cmd_output_bytes)}") ? |_| CommandFailed("stdout write failed") + + Ok({}) +} diff --git a/examples/dir.roc b/examples/dir.roc index a166520a..d2ed824d 100644 --- a/examples/dir.roc +++ b/examples/dir.roc @@ -1,42 +1,53 @@ 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! : List Arg => Result {} _ -main! = |_args| - - # Create a directory - Dir.create!("empty-dir")? - - dir_exists = File.is_dir!("empty-dir")? - expect dir_exists - - # Create a directory and its parents - Dir.create_all!("nested-dir/a/b/c")? - - # Create a child directory - Dir.create!("nested-dir/child")? - - # List the contents of a directory - paths_as_str = - Dir.list!("nested-dir") - |> Result.map_ok(|paths| List.map(paths, Path.display))? - - # Check the contents of the directory - expect Set.from_list(paths_as_str) == Set.from_list(["nested-dir/a", "nested-dir/child"]) - - # Delete an empty directory - Dir.delete_empty!("empty-dir")? - - # Delete all directories recursively - Dir.delete_all!("nested-dir")? - - Stdout.line!("Success!") +main! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + dir_result = || { + # Best-effort cleanup from a previous interrupted run. + Dir.delete_all!("empty-dir") ?? {} + Dir.delete_all!("nested-dir") ?? {} + + # Create a directory + Dir.create!("empty-dir")? + + # Create a directory and its parents + Dir.create_all!("nested-dir/a/b/c")? + + # Create a child directory + Dir.create!("nested-dir/child")? + + # List the contents of a directory + paths = Dir.list!("nested-dir")? + + # Check the contents of the directory + expect paths.len() == 2 + expect List.contains(paths.map(Path.display), "nested-dir/a") + expect List.contains(paths.map(Path.display), "nested-dir/child") + + # Delete an empty directory + Dir.delete_empty!("empty-dir")? + + # Delete all directories recursively + Dir.delete_all!("nested-dir")? + + Ok({}) + } + + match dir_result() { + Ok(_) => { + Stdout.line!("Success!") ? |_| Exit(1) + Ok({}) + } + Err(err) => { + Stderr.line!("Error during directory operations: ${Str.inspect(err)}") ? |_| Exit(1) + Err(Exit(1)) + } + } +} diff --git a/examples/env-var.roc b/examples/env-var.roc index a2b99e54..3bad126d 100644 --- a/examples/env-var.roc +++ b/examples/env-var.roc @@ -1,25 +1,24 @@ 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! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + result = Env.var!("EDITOR") -main! : List Arg => Result {} _ -main! = |_args| + match result { + Ok(editor) => { + Stdout.line!("Your favorite editor is ${editor}!") ? |_| Exit(1) + 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.") ? |_| Exit(1) + Ok({}) + } + } +} diff --git a/examples/error-handling.roc b/examples/error-handling.roc index 73261ff9..0d235b5a 100644 --- a/examples/error-handling.roc +++ b/examples/error-handling.roc @@ -1,30 +1,54 @@ 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! : List(Str) => Try({}, [Exit(I32), ..]) +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}") ? |_| Exit(1) + } + Err(FileErr(NotFound)) => { + Stdout.line!("Expected error: File not found (NotFound)") ? |_| Exit(1) + } + Err(FileErr(PermissionDenied)) => { + Stdout.line!("Error: Permission denied") ? |_| Exit(1) + } + Err(FileErr(Other(msg))) => { + Stdout.line!("Error: ${msg}") ? |_| Exit(1) + } + Err(_) => { + Stdout.line!("Error: Other file error") ? |_| Exit(1) + } + } + + # 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)? + + # Cleanup + File.delete!(file_name)? + + Ok(content) + } + + match file_result() { + Ok(content) => { + Stdout.line!("${file_name} contains: ${content}") ? |_| Exit(1) + Ok({}) + } + Err(_) => { + Stdout.line!("Error during file operations") ? |_| Exit(1) + Err(Exit(1)) + } + } +} diff --git a/examples/file-accessed-modified-created-time.roc b/examples/file-accessed-modified-created-time.roc index cb2452aa..b846a3dd 100644 --- a/examples/file-accessed-modified-created-time.roc +++ b/examples/file-accessed-modified-created-time.roc @@ -3,29 +3,27 @@ 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! : List(Str) => Try({}, _) +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)?) + # NOTE: these functions are checked and built in CI, but not run in release + # smoke tests because normal musl bundle builds do not support them + # consistently across targets. + 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 + )? + + Ok({}) +} diff --git a/examples/file-permissions.roc b/examples/file-permissions.roc index e2b0c2ee..b66c4ef3 100644 --- a/examples/file-permissions.roc +++ b/examples/file-permissions.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! : List(Str) => Try({}, _) +main! = |_args| { file = "LICENSE" is_executable = File.is_executable!(file)? @@ -17,10 +16,11 @@ 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)} + )? + + Ok({}) +} diff --git a/examples/file-read-buffered.roc b/examples/file-read-buffered.roc index 3801760e..110963ea 100644 --- a/examples/file-read-buffered.roc +++ b/examples/file-read-buffered.roc @@ -2,7 +2,6 @@ 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 @@ -19,13 +18,25 @@ import pf.Arg exposing [Arg] # # See examples/file-read-write.roc if you want to read the full contents at once. -main! : List Arg => Result {} _ -main! = |_args| - reader = File.open_reader!("LICENSE")? - - read_summary = process_line!(reader, { lines_read: 0, bytes_read: 0 })? - - Stdout.line!("Done reading file: ${Inspect.to_str(read_summary)}") +main! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + read_file = || { + reader = File.open_reader!("LICENSE")? + + process_line!(reader, { lines_read: 0, bytes_read: 0 }) + } + + match read_file() { + Ok(read_summary) => { + Stdout.line!("Done reading file: ${Str.inspect(read_summary)}") ? |_| Exit(1) + Ok({}) + } + Err(err) => { + Stdout.line!("Error during buffered file read: ${Str.inspect(err)}") ? |_| Exit(1) + Err(Exit(1)) + } + } +} ReadSummary : { lines_read : U64, @@ -33,20 +44,20 @@ ReadSummary : { } ## Count the number of lines and the number of bytes read. -process_line! : File.Reader, ReadSummary => Result ReadSummary _ +process_line! : File.Reader, ReadSummary => Try(ReadSummary, _) process_line! = |reader, { lines_read, bytes_read }| - when File.read_line!(reader) is - Ok(bytes) if List.len(bytes) == 0 -> + match File.read_line!(reader) { + Ok(bytes) if bytes.len() == 0 => Ok({ lines_read, bytes_read }) - Ok(bytes) -> + Ok(bytes) => process_line!( reader, { lines_read: lines_read + 1, - bytes_read: bytes_read + (List.len(bytes) |> Num.int_cast), + bytes_read: bytes_read + bytes.len(), }, ) - Err(err) -> - Err(ErrorReadingLine(Inspect.to_str(err))) + Err(err) => Err(err) + } diff --git a/examples/file-read-write.roc b/examples/file-read-write.roc index 13d60cdb..27f104e4 100644 --- a/examples/file-read-write.roc +++ b/examples/file-read-write.roc @@ -2,30 +2,34 @@ 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! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { out_file = "out.txt" - file_write_read!(out_file)? + Stdout.line!("Writing a string to out.txt") ? |_| Exit(1) - # 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| - - 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}\"") + contents = File.read_utf8!(out_file)? + # Cleanup + File.delete!(out_file)? + Ok(contents) + } + match file_result() { + Ok(contents) => { + Stdout.line!("I read the file back. Its contents are: \"${contents}\"") ? |_| Exit(1) + Ok({}) + } + Err(_) => { + Stdout.line!("Error during file operations") ? |_| Exit(1) + Err(Exit(1)) + } + } +} diff --git a/examples/file-size.roc b/examples/file-size.roc index 854ff76a..cef38e56 100644 --- a/examples/file-size.roc +++ b/examples/file-size.roc @@ -2,12 +2,14 @@ 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| - file_size = File.size_in_bytes!("LICENSE")? +main! : List(Str) => Try({}, _) +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")? + + Ok({}) +} diff --git a/examples/hello-world.roc b/examples/hello-world.roc index a4c7ec98..394ff073 100644 --- a/examples/hello-world.roc +++ b/examples/hello-world.roc @@ -1,10 +1,9 @@ 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! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + Stdout.line!("Hello, World!") ? |_| Exit(1) + Ok({}) +} diff --git a/examples/hello.roc b/examples/hello.roc new file mode 100644 index 00000000..ca9f2414 --- /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!") ? |_| Exit(1) + Ok({}) +} diff --git a/examples/http-client.roc b/examples/http-client.roc new file mode 100644 index 00000000..f28f069c --- /dev/null +++ b/examples/http-client.roc @@ -0,0 +1,81 @@ +app [main!] { + pf: platform "../platform/main.roc", + http: "https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst", +} + +import pf.Http +import pf.Stdout +import http.Request +import http.Response + +# 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| + match run_demo!() { + Ok({}) => Ok({}) + Err(message) => report_failure!(message) + } + +report_failure! : Str => Try({}, [Exit(I32), ..]) +report_failure! = |message| { + Stdout.line!("HTTP request failed: ${message}") ? |_| Exit(1) + Err(Exit(1)) +} + +run_demo! : () => Try({}, Str) +run_demo! = || { + utf8 = Http.get_utf8!("http://localhost:9000/utf8test") ? |_| "GET /utf8test failed" + write_line!("I received '${utf8}' from the server.")? + + request = Request.from_method(GET).with_uri("http://localhost:9000/utf8test") + response = Http.send!(request) ? |_| "send! failed" + status = U16.to_str(Response.status(response)) + + decoded : { foo : Str } + decoded = Http.get!("http://localhost:9000") ? |_| "GET / failed" + + write_line!("The json I received was: { foo: \"${decoded.foo}\" }")? + write_line!("send! returned status ${status}.")? + reject_invalid_json!()? + reject_invalid_utf8!()? + + Ok({}) +} + +reject_invalid_json! : () => Try({}, Str) +reject_invalid_json! = || { + result : Try({ foo : Str }, _) + result = Http.get!("http://localhost:9000/invalid-json") + + match result { + Err(JsonErr(_)) => write_line!("invalid JSON was rejected.") + Err(_) => Err("GET /invalid-json failed with the wrong error") + Ok(_) => Err("GET /invalid-json unexpectedly succeeded") + } +} + +reject_invalid_utf8! : () => Try({}, Str) +reject_invalid_utf8! = || { + result = Http.get_utf8!("http://localhost:9000/invalid-utf8") + + match result { + Err(BadBody(_)) => write_line!("invalid UTF-8 was rejected.") + Err(_) => Err("GET /invalid-utf8 failed with the wrong error") + Ok(_) => Err("GET /invalid-utf8 unexpectedly succeeded") + } +} + +write_line! : Str => Try({}, Str) +write_line! = |message| { + Stdout.line!(message) ? |_| "stdout write failed" + Ok({}) +} diff --git a/examples/http.roc b/examples/http.roc deleted file mode 100644 index f25907f8..00000000 --- a/examples/http.roc +++ /dev/null @@ -1,73 +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.Http -import pf.Stdout -import json.Json -import pf.Arg exposing [Arg] - -# Demo of all basic-cli Http functions - -# To run this example: -# ``` -# nix develop -# cd basic-cli/ci/rust_http_server -# cargo run -# ``` -# Then in another terminal: follow the steps in the README.md file of this folder. - -main! : List Arg => Result {} _ -main! = |_args| - - # # HTTP GET a String - # ---------------- - - hello_str : Str - hello_str = Http.get_utf8!("http://localhost:9000/utf8test")? - # If you want to see an example of the server side, see basic-cli/ci/rust_http_server/src/main.rs - - Stdout.line!("I received '${hello_str}' from the server.\n")? - - # # Getting json - # ------------ - - # We decode/deserialize the json `{ "foo": "something" }` into a Roc record - - { foo } = Http.get!("http://localhost:9000", Json.utf8)? - # If you want to see an example of the server side, see basic-cli/ci/rust_http_server/src/main.rs - - Stdout.line!("The json I received was: { foo: \"$(foo)\" }\n")? - - # # Getting a Response record - # ------------------------- - - response : Http.Response - response = Http.send!( - { - method: GET, - headers: [], - uri: "https://www.example.com", - body: [], - timeout_ms: TimeoutMilliseconds(5000), - }, - )? - - body_str = (Str.from_utf8(response.body))? - - Stdout.line!("Response body:\n\t${body_str}.\n")? - - # # Using default_request and providing a header - # -------------------------------------------- - - response_2 = - Http.default_request - |> &uri "https://www.example.com" - |> &headers [Http.header(("Accept", "text/html"))] - |> Http.send!()? - - body_str_2 = (Str.from_utf8(response_2.body))? - - # Same as above - Stdout.line!("Response body 2:\n\t${body_str_2}.\n") diff --git a/examples/locale.roc b/examples/locale.roc index fe151722..1e7010ae 100644 --- a/examples/locale.roc +++ b/examples/locale.roc @@ -1,18 +1,21 @@ 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! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + locale_str = match Locale.get!() { + Ok(locale) => locale + Err(NotAvailable) => "" + } + Stdout.line!("The most preferred locale for this system or application: ${locale_str}") ? |_| Exit(1) -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, ", ") + Stdout.line!("All available locales for this system or application: [${locales_str}]") ? |_| Exit(1) - 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..8628f187 100644 --- a/examples/path.roc +++ b/examples/path.roc @@ -2,27 +2,19 @@ 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! : List(Str) => Try({}, [Exit(I32), ..]) +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)} - """ - ) + \\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)} + ) ? |_| Exit(1) + + Ok({}) +} diff --git a/examples/print.roc b/examples/print.roc index 433de1c1..721d9e40 100644 --- a/examples/print.roc +++ b/examples/print.roc @@ -2,30 +2,27 @@ 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! : List Arg => Result {} _ -main! = |_args| - - # # Print a string to stdout +main! : List(Str) => Try({}, _) +main! = |_args| { + # Print a string to stdout Stdout.line!("Hello, world!")? - # # Print without a newline + # Print without a newline Stdout.write!("No newline after me.")? - # # Print a string to stderr + # Print a string to stderr Stderr.line!("Hello, error!")? - # # Print a string to stderr without a newline + # Print a string to stderr without a newline Stderr.write!("Err with no newline after.")? - # # 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. + # Print a list to stdout + for str in ["Foo", "Bar", "Baz"] { + Stdout.line!(str)? + } + + Ok({}) +} diff --git a/examples/random.roc b/examples/random.roc index b539f9c8..44d48ecb 100644 --- a/examples/random.roc +++ b/examples/random.roc @@ -1,20 +1,21 @@ 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! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + result = Random.seed_u64!() + match result { + Ok(random_u64) => { + Stdout.line!("Random U64 seed is: ${random_u64.to_str()}") ? |_| Exit(1) + Ok({}) + } + Err(_) => { + Stdout.line!("Failed to generate random seed") ? |_| Exit(1) + Err(Exit(1)) + } + } +} diff --git a/examples/sqlite-basic.roc b/examples/sqlite-basic.roc index f9c37db2..b08ce386 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,89 @@ import pf.Arg exposing [Arg] # status TEXT NOT NULL # ); -main! : List Arg => Result {} _ +main! : List(Str) => Try({}, [Exit(I32), ..]) main! = |_args| - db_path = Env.var!("DB_PATH")? + match run!() { + Ok(_) => Ok({}) + Err(_) => Err(Exit(1)) + } - todos = query_todos_by_status!(db_path, "todo")? +run! : () => Try({}, [SqliteExampleFailed(Str)]) +run! = || { + db_path = + match Env.var!("DB_PATH") { + Ok(p) => p + Err(_) => "./examples/todos.db" + } - Stdout.line!("All Todos:")? + todos = query_todos_by_status!(db_path, "todo") ? |err| SqliteExampleFailed(Str.inspect(err)) - # print todos - List.for_each_try!( - todos, - |{ id, task, status }| - Stdout.line!("\tid: ${id}, task: ${task}, status: ${Inspect.to_str(status)}"), - )? + print_line!("All Todos:")? - completed_todos = query_todos_by_status!(db_path, "completed")? + for todo in todos { + print_todo!(todo)? + } - Stdout.line!("\nCompleted Todos:")? - List.for_each_try!( - completed_todos, - |{ id, task, status }| - Stdout.line!("\tid: ${id}, task: ${task}, status: ${Inspect.to_str(status)}"), - ) + completed_todos = query_todos_by_status!(db_path, "completed") ? |err| SqliteExampleFailed(Str.inspect(err)) + + print_line!("")? + print_line!("Completed Todos:")? + for todo in completed_todos { + 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 => Try({}, [SqliteExampleFailed(Str)]) +print_todo! = |todo| + print_line!(" id: ${todo.id}, task: ${todo.task}, status: ${status_to_str(todo.status)}") + +print_line! : Str => Try({}, [SqliteExampleFailed(Str)]) +print_line! = |line| { + Stdout.line!(line) ? |_| SqliteExampleFailed("stdout write failed") + Ok({}) +} + 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)? + match decode_todo_status(status_str) { + Ok(status) => Ok({ id: I64.to_str(id), task, status }) + Err(ParseError(message)) => Err(ParseError(message)) + } + } + 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..59e1284e 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,66 @@ 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! : List(Str) => Try({}, [Exit(I32), ..]) main! = |_args| - db_path = Env.var!("DB_PATH")? + match run!() { + Ok(_) => Ok({}) + Err(_) => Err(Exit(1)) + } + +run! : () => Try({}, [SqliteExampleFailed(Str)]) +run! = || { + 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)}"), - )? + ) ? |err| SqliteExampleFailed(Str.inspect(err)) - # Example: filter rows by status + print_line!("All Todos:")? + for t in all_todos { + 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"), }, - )? + ) ? |err| SqliteExampleFailed(Str.inspect(err)) - Stdout.line!("\nIn-progress Todos:")? - - List.for_each_try!( - tasks_in_progress, - |task_description| - Stdout.line!("\tIn-progress tasks: ${task_description}"), - )? + print_line!("")? + print_line!("In-progress Todos:")? + for task in tasks_in_progress { + 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) }, + ], + }, + ) ? |err| SqliteExampleFailed(Str.inspect(err)) # 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 +87,180 @@ 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, + }, + ) ? |err| SqliteExampleFailed(Str.inspect(err)) # 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) }, + ], + }, + ) ? |err| SqliteExampleFailed(Str.inspect(err)) # Example: delete a row + Sqlite.execute!( + { + path: db_path, + query: "DELETE FROM todos WHERE task = :task;", + bindings: [{ name: ":task", value: String("Make sql example.") }], + }, + ) ? |err| SqliteExampleFailed(Str.inspect(err)) - 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) }], + }, + ) ? |err| SqliteExampleFailed(Str.inspect(err)) # Example: count the number of rows + count = Sqlite.query!( + { + path: db_path, + query: "SELECT COUNT(*) as \"count\" FROM todos;", + bindings: [], + row: Sqlite.u64("count"), + }, + ) ? |err| SqliteExampleFailed(Str.inspect(err)) - count = Sqlite.query!({ - path: db_path, - query: "SELECT COUNT(*) as \"count\" FROM todos;", - bindings: [], - row: Sqlite.u64("count"), - })? - - expect count == 3 + print_line!("")? + print_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);", }, - })? + ) ? |err| SqliteExampleFailed(Str.inspect(err)) - Stdout.line!("\nTodos sorted by length of task description:")? + todos_sorted = Sqlite.query_many_prepared!( + { + stmt: prepared_query, + bindings: [], + rows: decode_task_status, + }, + ) ? |err| SqliteExampleFailed(Str.inspect(err)) - List.for_each_try!( - todos_sorted, - |{ task, status }| - Stdout.line!("\t task: ${task}, status: ${Inspect.to_str(status)}"), - )? + print_line!("")? + print_line!("Todos sorted by length of task description:")? + for t in todos_sorted { + print_line!(" task: ${t.task}, status: ${status_to_str(t.status)}")? + } + + Ok({}) +} +print_line! : Str => Try({}, [SqliteExampleFailed(Str)]) +print_line! = |s| { + Stdout.line!(s) ? |_| SqliteExampleFailed("stdout write failed") Ok({}) +} + +# 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)? + match decode_status(status_str) { + Ok(status) => { + edited_val = Sqlite.nullable_i64("edited")(cols)(stmt)? + Ok({ id, task, status, edited_val }) + } + Err(ParseError(message)) => Err(ParseError(message)) + } + } + +# 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)? + match decode_status(status_str) { + Ok(status) => Ok({ task, status }) + Err(ParseError(message)) => Err(ParseError(message)) + } + } 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..ce25897a 100644 --- a/examples/stdin-basic.roc +++ b/examples/stdin-basic.roc @@ -2,22 +2,15 @@ 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 +main! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + Stdout.line!("What's your first name?") ? |_| Exit(1) + first = Stdin.line!() ?? "" -# Reading text from stdin. -# If you want to read Stdin from a pipe, check out examples/stdin-pipe.roc + Stdout.line!("What's your last name?") ? |_| Exit(1) + last = Stdin.line!() ?? "" -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}! 👋") + Stdout.line!("Hi, ${first} ${last}! \u(1F44B)") ? |_| Exit(1) + Ok({}) +} diff --git a/examples/stdin-pipe.roc b/examples/stdin-pipe.roc index 80d18140..19e47ac6 100644 --- a/examples/stdin-pipe.roc +++ b/examples/stdin-pipe.roc @@ -2,18 +2,18 @@ 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! : List(Str) => Try({}, _) +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 = Stdin.read_to_end!()? piped_in_str = Str.from_utf8(piped_in)? - Stdout.line!("This is what you piped in: \"${piped_in_str}\"") + Stdout.line!("This is what you piped in: \"${piped_in_str}\"")? + Ok({}) +} diff --git a/examples/tcp-client.roc b/examples/tcp-client.roc index 6276cd84..4f4c039d 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), - ) - |> 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)}") + match Tcp.connect!("127.0.0.1", 8085) { + Ok(stream) => { + Stdout.line!("Connected!") ? |_| Exit(1) + 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!("> ") ? |_| Exit(1) + 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}") ? |_| Exit(1) + 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 + ) ? |_| Exit(1) + 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") ? |_| Exit(1) + 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}") ? |_| Exit(1) + Ok({}) +} diff --git a/examples/temp-dir.roc b/examples/temp-dir.roc index fc4e7c05..b6999298 100644 --- a/examples/temp-dir.roc +++ b/examples/temp-dir.roc @@ -3,18 +3,15 @@ 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 # Prints the default temp dir -# -# !! this requires the flag `--linker=legacy`: -# for example: `roc build examples/temp-dir.roc --linker=legacy` -main! : List Arg => Result {} _ -main! = |_args| +main! : List(Str) => Try({}, _) +main! = |_args| { + temp_dir_path = Env.temp_dir!() - temp_dir_path_str = Path.display(Env.temp_dir!({})) - - Stdout.line!("The temp dir path is ${temp_dir_path_str}") + Stdout.line!("The temp dir path is ${Path.display(temp_dir_path)}")? + Ok({}) +} diff --git a/examples/terminal-app-snake.roc b/examples/terminal-app-snake.roc index 2a593553..ed53e1ca 100644 --- a/examples/terminal-app-snake.roc +++ b/examples/terminal-app-snake.roc @@ -3,228 +3,255 @@ 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. +# 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_lst : NonEmptyList, - food_pos : Position, - direction : [Up, Down, Left, Right], + snake : Snake, + food : Position, + direction : Direction, 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 : GameState 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, + 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, } -# Keep this above 15 for the initial food_pos +grid_size : I64 grid_size = 20 -init_snake_len = len(initial_state.snake_lst) +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 Arg => Result {} _ -main! = |_args| - Tty.enable_raw_mode!({}) +main! : List(Str) => Try({}, [Exit(I32)]) +main! = |args| { + Tty.enable_raw_mode!() + game_result = game_loop!(initial_state_from_args(args)) + Tty.disable_raw_mode!() - game_loop!(initial_state)? + match game_result { + Ok({}) => Stdout.line!("\n--- Game Over ---").map_err(|_| Exit(1)) + Err(err) => Err(err) + } +} - Tty.disable_raw_mode!({}) - Stdout.line!("\n--- Game Over ---") +initial_state_from_args : List(Str) -> GameState +initial_state_from_args = |args| { + # Avoid specializing the renderer with a fully known initial state; the + # current compiler postcheck panics on that path. + has_args = args.len() > 0 + { ..initial_state, game_over: has_args and Bool.not(has_args) } +} -game_loop! : GameState => Result {} _ -game_loop! = |state| - if state.game_over then +game_loop! : GameState => Try({}, [Exit(I32)]) +game_loop! = |state| { + if state.game_over { Ok({}) - else + } else { draw_game!(state)? - # Check keyboard input - input_bytes = Stdin.bytes!({})? + input_bytes = Stdin.bytes!().map_err(|_| Exit(1))? + new_state = update_game(apply_input(state, input_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) + } +} + +apply_input : GameState, List(U8) -> GameState +apply_input = |state, input_bytes| { + for byte in input_bytes { + return apply_input_byte(state, byte) + } + + state +} + +apply_input_byte : GameState, U8 -> GameState +apply_input_byte = |state, 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 + } update_game : GameState -> GameState -update_game = |state| - if state.game_over then +update_game = |state| { + if state.game_over { 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 } + } 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 -draw_game! : GameState => Result {} _ -draw_game! = |state| - clear_screen!({})? +move_head : Position, Direction -> Position +move_head = |head, direction| + { x: head.x + direction.dx, y: head.y + direction.dy } - Stdout.line!("\nControls: W A S D to move, Q to quit\n\r")? +draw_game! : GameState => Try({}, [Exit(I32)]) +draw_game! = |state| { + clear_screen!()? - # \r to fix indentation because we're in raw mode - Stdout.line!("Score: ${Num.to_str(len(state.snake_lst) - init_snake_len)}\r")? + 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") + Stdout.line!("${rendered_game_str}\r").map_err(|_| Exit(1)) +} 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 + 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 positions_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 positions_contains(snake.rest, pos) + +positions_contains : List(Position), Position -> Bool +positions_contains = |positions, pos| { + for current in positions { + if current == pos { + return Bool.True + } + } + + Bool.False +} + +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 + snake.rest.len() + +initial_grid = + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\........ooO......... + \\.................... + \\.................... + \\.................... + \\.................... + \\...............*.... + \\.................... + \\.................... + \\.................... + \\.................... + +moved_down_grid = + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.................... + \\.........oo......... + \\..........O......... + \\.................... + \\.................... + \\.................... + \\...............*.... + \\.................... + \\.................... + \\.................... + \\.................... diff --git a/examples/time.roc b/examples/time.roc index 07c330a0..64a6c9e0 100644 --- a/examples/time.roc +++ b/examples/time.roc @@ -3,21 +3,22 @@ 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| - start = Utc.now!({}) +main! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + start = Utc.now!() # 1000 ms = 1 second Sleep.millis!(1000) - finish = Utc.now!({}) + finish = Utc.now!() + + duration_ms = Utc.delta_as_millis(finish, start) + duration_nanos = Utc.delta_as_nanos(finish, start) - duration = Num.to_str(Utc.delta_as_nanos(start, finish)) + Stdout.line!("Completed in ${duration_ms.to_str()} ms (${duration_nanos.to_str()} ns)") ? |_| Exit(1) - Stdout.line!("Completed in ${duration} ns") + Ok({}) +} diff --git a/examples/tty.roc b/examples/tty.roc new file mode 100644 index 00000000..630145d8 --- /dev/null +++ b/examples/tty.roc @@ -0,0 +1,18 @@ +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! : List(Str) => Try({}, [Exit(I32), ..]) +main! = |_args| { + Stdout.line!("Tty: enabling raw mode") ? |_| Exit(1) + Tty.enable_raw_mode!() + + Stdout.line!("Tty: disabling raw mode") ? |_| Exit(1) + 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..239ceb47 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -1,238 +1,276 @@ -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 IOErr 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 } +Cmd :: { + args : List(Str), + clear_envs : Bool, + envs : List((Str, Str)), + program : Str, +}.{ + ## 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 = Host.cmd_exec_output!(to_host_cmd(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 = Host.cmd_exec_output!(to_host_cmd(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 Host.cmd_exec_exit_code!(to_host_cmd(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| { + { ..cmd, envs: cmd.envs.append((key, value)) } + } + + ## 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| { ..cmd, envs: cmd.envs.concat(pairs) } + + ## 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| { ..cmd, clear_envs: Bool.True } +} + +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 = { + envs = cmd.envs.map(|(key, value)| "${key}=${value}") + my_trim(Str.trim(Str.join_with(envs, " "))) + } + + clear_envs_str = if cmd.clear_envs { ", clear_envs: true" } else { "" } + args_str = Str.join_with(cmd.args, " ") + + "{ cmd: ${cmd.program}, args: ${args_str}${envs_str}${clear_envs_str} }" +} -## 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) }) +to_host_cmd : Cmd -> Host.Cmd +to_host_cmd = |cmd| { + args: cmd.args, + clear_envs: cmd.clear_envs, + envs: flatten_str_pairs(cmd.envs, [], 0), + program: cmd.program, +} diff --git a/platform/Dir.roc b/platform/Dir.roc index a359fe06..5767e646 100644 --- a/platform/Dir.roc +++ b/platform/Dir.roc @@ -1,72 +1,41 @@ -module [ - IOErr, - list!, - create!, - create_all!, - delete_empty!, - delete_all!, -] +import IOErr exposing [IOErr] +import Host +import path.Path as PackagePath -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), ..]) + create! = |path| Ok(Host.dir_create!(path)?) -## 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), ..]) + create_all! = |path| Ok(Host.dir_create_all!(path)?) -## 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), ..]) + delete_empty! = |path| Ok(Host.dir_delete_empty!(path)?) -## 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), ..]) + delete_all! = |path| Ok(Host.dir_delete_all!(path)?) -## 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 byte-preserving paths of all files and directories within + ## the specified directory. + list! : Str => Try(List(PackagePath.Path), [DirErr(IOErr), ..]) + list! = |path| { + bytes_list = Host.dir_list!(path)? + Ok(bytes_list.map(|bytes| PackagePath.unix_bytes(bytes))) + } +} diff --git a/platform/Env.roc b/platform/Env.roc index 1253bc82..d479f8df 100644 --- a/platform/Env.roc +++ b/platform/Env.roc @@ -1,171 +1,36 @@ -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 +import path.Path as PackagePath + +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), ..]) + var! = |name| Ok(Host.env_var!(name)?) + + ## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory) + ## from the environment. + ## + ## Returns `Err(CwdUnavailable)` if the cwd cannot be determined. + cwd! : () => Try(PackagePath.Path, [CwdUnavailable, ..]) + cwd! = || { + bytes = Host.env_cwd!()? + Ok(PackagePath.unix_bytes(bytes)) + } + + ## Gets the path to the currently-running executable. + ## + ## Returns `Err(ExePathUnavailable)` if the path cannot be determined. + exe_path! : () => Try(PackagePath.Path, [ExePathUnavailable, ..]) + exe_path! = || { + bytes = Host.env_exe_path!()? + Ok(PackagePath.unix_bytes(bytes)) + } + + ## Gets the default directory for temporary files. + temp_dir! : () => PackagePath.Path + temp_dir! = || PackagePath.unix_bytes(Host.env_temp_dir!()) +} 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..121d30d3 100644 --- a/platform/File.roc +++ b/platform/File.roc @@ -1,355 +1,81 @@ -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 Path exposing [Path] -import InternalIOErr +import IOErr exposing [IOErr] import Host -import InternalPath -import Utc exposing [Utc] - -## 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 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) - -## 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)) - -## 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)) - -## 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)) - -## 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)) - -## 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)) - - -## 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 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 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))) +File := [].{ + ## Represents a buffered file reader. + ## + ## The file is automatically closed when the last reference to the reader is + ## dropped. This is an opaque `Box(U64)` handle into a host-side + ## `BufReader`. + Reader : Host.FileReader + + ## Read all bytes from a file. + read_bytes! : Str => Try(List(U8), [FileErr(IOErr), ..]) + read_bytes! = |path| Ok(Host.file_read_bytes!(path)?) + + ## Write bytes to a file, replacing any existing contents. + write_bytes! : Str, List(U8) => Try({}, [FileErr(IOErr), ..]) + write_bytes! = |path, bytes| Ok(Host.file_write_bytes!(path, bytes)?) + + ## 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), ..]) + read_utf8! = |path| Ok(Host.file_read_utf8!(path)?) + + ## Write a UTF-8 string to a file, replacing any existing contents. + write_utf8! : Str, Str => Try({}, [FileErr(IOErr), ..]) + write_utf8! = |path, content| Ok(Host.file_write_utf8!(path, content)?) + + ## Open a file for buffered reading using the default buffer capacity. + ## + ## ```roc + ## reader = File.open_reader!("LICENSE")? + ## line = File.read_line!(reader)? + ## ``` + open_reader! = |path| + Ok(Host.file_open_reader!(path, 0)?) + + ## Open a file for buffered reading using a specific buffer capacity. + open_reader_with_capacity! = |path, capacity| + Ok(Host.file_open_reader!(path, capacity)?) + + ## Read bytes up to and including the next newline from a buffered reader. + ## + ## Returns an empty list at EOF. + read_line! = |reader| + Ok(Host.file_read_line!(reader)?) + + ## Delete a file. + delete! : Str => Try({}, [FileErr(IOErr), ..]) + delete! = |path| Ok(Host.file_delete!(path)?) + + ## Returns the size of a file in bytes. + size_in_bytes! : Str => Try(U64, [FileErr(IOErr), ..]) + size_in_bytes! = |path| Ok(Host.file_size_in_bytes!(path)?) + + ## Checks if the file has any executable bit set. + is_executable! : Str => Try(Bool, [FileErr(IOErr), ..]) + is_executable! = |path| Ok(Host.file_is_executable!(path)?) + + ## Checks if the file has a readable owner permission bit set. + is_readable! : Str => Try(Bool, [FileErr(IOErr), ..]) + is_readable! = |path| Ok(Host.file_is_readable!(path)?) + + ## Checks if the file has a writable owner permission bit set. + is_writable! : Str => Try(Bool, [FileErr(IOErr), ..]) + is_writable! = |path| Ok(Host.file_is_writable!(path)?) + + ## Returns the time when the file was last accessed as nanoseconds since the Unix epoch. + time_accessed! : Str => Try(U128, [FileErr(IOErr), ..]) + time_accessed! = |path| Ok(Host.file_time_accessed!(path)?) + + ## Returns the time when the file was last modified as nanoseconds since the Unix epoch. + time_modified! : Str => Try(U128, [FileErr(IOErr), ..]) + time_modified! = |path| Ok(Host.file_time_modified!(path)?) + + ## Returns the time when the file was created as nanoseconds since the Unix epoch. + time_created! : Str => Try(U128, [FileErr(IOErr), ..]) + time_created! = |path| Ok(Host.file_time_created!(path)?) +} diff --git a/platform/Host.roc b/platform/Host.roc index 4e83fdc7..535a0e1d 100644 --- a/platform/Host.roc +++ b/platform/Host.roc @@ -1,153 +1,106 @@ -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 IOErr exposing [IOErr] 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 + +Host := [].{ + Cmd : { + args : List(Str), + clear_envs : Bool, + envs : List(Str), + program : Str, + } + + CmdOutputSuccess : { + stderr_bytes : List(U8), + stdout_bytes : List(U8), + } + + CmdOutputFailure : { + stderr_bytes : List(U8), + stdout_bytes : List(U8), + exit_code : I32, + } + + FileReader :: Box(U64) + + PathType : { + is_dir : Bool, + is_file : Bool, + is_sym_link : Bool, + } + + SqliteStmt :: Box(U64) + + TcpStream :: Box(U64) + + cmd_exec_exit_code! : Cmd => Try(I32, IOErr) + cmd_exec_output! : Cmd => Try(CmdOutputSuccess, Try(CmdOutputFailure, IOErr)) + + dir_create! : Str => Try({}, [DirErr(IOErr)]) + dir_create_all! : Str => Try({}, [DirErr(IOErr)]) + dir_delete_empty! : Str => Try({}, [DirErr(IOErr)]) + dir_delete_all! : Str => Try({}, [DirErr(IOErr)]) + dir_list! : Str => Try(List(List(U8)), [DirErr(IOErr)]) + + env_var! : Str => Try(Str, [VarNotFound(Str)]) + env_cwd! : () => Try(List(U8), [CwdUnavailable]) + env_exe_path! : () => Try(List(U8), [ExePathUnavailable]) + env_temp_dir! : () => List(U8) + + file_read_bytes! : Str => Try(List(U8), [FileErr(IOErr)]) + file_write_bytes! : Str, List(U8) => Try({}, [FileErr(IOErr)]) + file_read_utf8! : Str => Try(Str, [FileErr(IOErr)]) + file_write_utf8! : Str, Str => Try({}, [FileErr(IOErr)]) + file_open_reader! : Str, U64 => Try(FileReader, [FileErr(IOErr)]) + file_read_line! : FileReader => Try(List(U8), [FileErr(IOErr)]) + file_delete! : Str => Try({}, [FileErr(IOErr)]) + file_size_in_bytes! : Str => Try(U64, [FileErr(IOErr)]) + file_is_executable! : Str => Try(Bool, [FileErr(IOErr)]) + file_is_readable! : Str => Try(Bool, [FileErr(IOErr)]) + file_is_writable! : Str => Try(Bool, [FileErr(IOErr)]) + file_time_accessed! : Str => Try(U128, [FileErr(IOErr)]) + file_time_modified! : Str => Try(U128, [FileErr(IOErr)]) + file_time_created! : Str => Try(U128, [FileErr(IOErr)]) + + http_send_request! : InternalHttp.RequestToAndFromHost => Try(InternalHttp.ResponseToAndFromHost, InternalHttp.TransportErr) + + locale_get! : () => Try(Str, [NotAvailable]) + locale_all! : () => List(Str) + + path_type! : List(U8) => Try(PathType, IOErr) + + random_seed_u64! : () => Try(U64, [RandomErr(IOErr)]) + random_seed_u32! : () => Try(U32, [RandomErr(IOErr)]) + + sleep_millis! : U64 => {} + + sqlite_prepare! : Str, Str => Try(SqliteStmt, InternalSqlite.SqliteError) + sqlite_bind! : SqliteStmt, List(InternalSqlite.SqliteBindings) => Try({}, InternalSqlite.SqliteError) + sqlite_columns! : SqliteStmt => List(Str) + sqlite_column_value! : SqliteStmt, U64 => Try(InternalSqlite.SqliteValue, InternalSqlite.SqliteError) + sqlite_step! : SqliteStmt => Try(Bool, InternalSqlite.SqliteError) + sqlite_reset! : SqliteStmt => Try({}, InternalSqlite.SqliteError) + + stderr_line! : Str => Try({}, [StderrErr(IOErr)]) + stderr_write! : Str => Try({}, [StderrErr(IOErr)]) + stderr_write_bytes! : List(U8) => Try({}, [StderrErr(IOErr)]) + + stdin_line! : () => Try(Str, [EndOfFile, StdinErr(IOErr)]) + stdin_bytes! : () => Try(List(U8), [EndOfFile, StdinErr(IOErr)]) + stdin_read_to_end! : () => Try(List(U8), [StdinErr(IOErr)]) + + stdout_line! : Str => Try({}, [StdoutErr(IOErr)]) + stdout_write! : Str => Try({}, [StdoutErr(IOErr)]) + stdout_write_bytes! : List(U8) => Try({}, [StdoutErr(IOErr)]) + + tcp_connect! : Str, U16 => Try(TcpStream, Str) + tcp_read_up_to! : TcpStream, U64 => Try(List(U8), Str) + tcp_read_exactly! : TcpStream, U64 => Try(List(U8), Str) + tcp_read_until! : TcpStream, U8 => Try(List(U8), Str) + tcp_write! : TcpStream, List(U8) => Try({}, Str) + + tty_enable_raw_mode! : () => {} + tty_disable_raw_mode! : () => {} + + utc_now! : () => U128 +} diff --git a/platform/Http.roc b/platform/Http.roc index 433125f2..3e82848e 100644 --- a/platform/Http.roc +++ b/platform/Http.roc @@ -1,146 +1,53 @@ -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, +import InternalHttp +import http.Request +import http.Response + +Http := [].{ + ## Errors raised by the host while sending a request, before a real HTTP + ## response is available. + TransportErr : InternalHttp.TransportErr + + ## Send an HTTP request, succeeding with a [Response] or failing with an + ## `HttpErr`. + ## + ## ```roc + ## request = Request.from_method(GET).with_uri("https://www.roc-lang.org") + ## response = Http.send!(request)? + ## ``` + send! : Request.Request => Try(Response.Response, [HttpErr(TransportErr), ..]) + send! = |request| { + host_response = Host.http_send_request!(InternalHttp.to_host_request(request)) ? HttpErr + + Ok(InternalHttp.from_host_response(host_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(TransportErr), ..]) + get_utf8! = |uri| { + response = send!(Request.from_method(GET).with_uri(uri))? + body = Str.from_utf8(Response.body(response)) ? |_| BadBody("get_utf8!: response body was not valid UTF-8") + + Ok(body) + } + + ## Perform an HTTP GET and decode the response body as JSON. + ## + ## JSON parser failures are returned as `JsonErr(_)`. + ## + ## ```roc + ## payload : Try({ foo : Str }, _) + ## payload = Http.get!("http://localhost:8000") + ## ``` + get! : Str => Try(_, [BadBody(Str), HttpErr(TransportErr), JsonErr(_), ..]) + get! = |uri| { + body = get_utf8!(uri)? + decoded = Json.parse(body) ? JsonErr + + Ok(decoded) + } } - -## 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)}")) 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..0207eece 100644 --- a/platform/InternalHttp.roc +++ b/platform/InternalHttp.roc @@ -1,139 +1,88 @@ -# 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, +import http.Header +import http.Method +import http.Request +import http.Response + +# Host-ABI types and conversions shared between the host and the Http module. +# These records map 1:1 to the generated Rust glue types in src/roc_platform_abi.rs. +InternalHttp := [].{ + ## Errors raised by the host while sending a request, before a real HTTP + ## response is available. + TransportErr : [Timeout, NetworkError, BadBody, Other(List(U8))] + + # Generated Rust glue uses tuple headers at the host ABI boundary. + HostHeaderTuple : (Str, Str) + + RequestToAndFromHost : { + method : U8, + method_ext : Str, + headers : List(HostHeaderTuple), + uri : Str, + body : List(U8), + timeout_ms : U64, + } + + ResponseToAndFromHost : { + status : U16, + headers : List(HostHeaderTuple), + body : List(U8), + } + + to_host_request : Request.Request -> RequestToAndFromHost + to_host_request = |request| { + method = Request.method(request) + { + method: to_host_method(method), + method_ext: to_host_method_ext(method), + headers: to_host_headers(Request.headers(request)), + uri: Request.uri(request), + body: Request.body(request), + timeout_ms: to_host_timeout(Request.timeout(request)), + } + } + + from_host_response : ResponseToAndFromHost -> Response.Response + from_host_response = |response| + Response.from_status(response.status) + .with_headers(from_host_headers(response.headers)) + .with_body(response.body) + + to_host_headers : List(Header.Header) -> List(HostHeaderTuple) + to_host_headers = |headers| + headers.map(|{ name, value }| (name, value)) + + from_host_headers : List(HostHeaderTuple) -> List(Header.Header) + from_host_headers = |headers| + headers.map(|(name, value)| { name, value }) } -# 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.Method -> U8 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 + match method { + OPTIONS => 5 + GET => 3 + POST => 7 + PUT => 8 + DELETE => 1 + HEAD => 4 + TRACE => 9 + CONNECT => 0 + PATCH => 6 + QUERY => 2 + Unknown(_) => 2 + } + +to_host_method_ext : Method.Method -> Str to_host_method_ext = |method| - when method is - EXTENSION(ext) -> ext - _ -> "" + match method { + QUERY => "QUERY" + Unknown(ext) => ext + _ => "" + } -to_host_timeout : _ -> U64 +to_host_timeout : [TimeoutMilliseconds(U64), NoTimeout] -> 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, -} + match timeout { + TimeoutMilliseconds(ms) => ms + NoTimeout => 0 + } 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..9f560d64 100644 --- a/platform/Locale.roc +++ b/platform/Locale.roc @@ -1,20 +1,17 @@ -module [ - get!, - all!, -] - 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) +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, ..]) + get! = || Ok(Host.locale_get!()?) -## 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`s are BCP 47 language tags, like `en-US` or `fr-CA`. + all! : () => List(Str) + all! = || Host.locale_all!() +} diff --git a/platform/Path.roc b/platform/Path.roc index 58dc7226..64ff3fa7 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -1,417 +1,119 @@ -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 IOErr exposing [IOErr] 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 path.Path as PackagePath + +Path := [].{ + ## Create a path from a Roc string using the host platform's native path representation. + ## + ## basic-cli currently targets Unix-like hosts, so this stores the string's UTF-8 bytes + ## as a Unix path. + from_str : Str -> PackagePath.Path + from_str = |str| PackagePath.unix(str) + + ## Create a Unix path from a Roc string by storing its UTF-8 bytes. + unix : Str -> PackagePath.Path + unix = |str| PackagePath.unix(str) + + ## Create a Unix path from raw bytes without validating UTF-8. + unix_bytes : List(U8) -> PackagePath.Path + unix_bytes = |bytes| PackagePath.unix_bytes(bytes) + + ## Create a Windows path from a Roc string by storing its UTF-16 code units. + windows : Str -> PackagePath.Path + windows = |str| PackagePath.windows(str) + + ## Create a Windows path from raw UTF-16 code units. + windows_u16s : List(U16) -> PackagePath.Path + windows_u16s = |u16s| PackagePath.windows_u16s(u16s) + + ## Convert a path to a string if its raw representation is valid text. + to_str : PackagePath.Path -> Try(Str, [InvalidStr(U64)]) + to_str = |path| path.to_str() + + ## Convert a path to a display string, replacing invalid text with U+FFFD. + display : PackagePath.Path -> Str + display = |path| path.display() + + ## Returns everything after the last directory separator. + filename : PackagePath.Path -> Try(PackagePath.Path, [IsDirPath, EndsInDots]) + filename = |path| path.filename() + + ## Returns the filename extension without the leading dot. + ext : PackagePath.Path -> Try(PackagePath.Path, [IsDirPath, EndsInDots]) + ext = |path| path.ext() + + ## Adds a separator and a string component to the path. + join : PackagePath.Path, Str -> PackagePath.Path + join = |path, str| path.join(str) + + ## Expose the raw OS-specific representation. + to_raw : PackagePath.Path -> [UnixBytes(List(U8)), WindowsU16s(List(U16))] + to_raw = |path| path.to_raw() + + ## Build a path from the raw OS-specific representation. + from_raw : [UnixBytes(List(U8)), WindowsU16s(List(U16))] -> PackagePath.Path + from_raw = |raw| PackagePath.from_raw(raw) + + ## 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! : PackagePath.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! : PackagePath.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! : PackagePath.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) + } + + ## 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 value. + type! : PackagePath.Path => Try([IsFile, IsDir, IsSymLink], [PathErr(IOErr), ..]) + type! = |path| { + # TODO: once https://github.com/roc-lang/roc/issues/9864 is fixed, + # app authors should be able to call this effect as `path.type!()`. + 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 + } + }) + } +} + +to_bytes : PackagePath.Path -> List(U8) +to_bytes = |path| + match PackagePath.to_raw(path) { + UnixBytes(bytes) => bytes + WindowsU16s(_) => Str.to_utf8(PackagePath.display(path)) + } diff --git a/platform/Random.roc b/platform/Random.roc index ac11df2b..6c7d58bf 100644 --- a/platform/Random.roc +++ b/platform/Random.roc @@ -1,52 +1,12 @@ -module [ - IOErr, - random_seed_u64!, - random_seed_u32!, -] - -import InternalIOErr +import IOErr exposing [IOErr] import Host +Random := [].{ + ## Generate a random 64-bit unsigned integer seed. + seed_u64! : () => Try(U64, [RandomErr(IOErr), ..]) + seed_u64! = || Ok(Host.random_seed_u64!()?) -## 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), ..]) + seed_u32! = || Ok(Host.random_seed_u32!()?) +} diff --git a/platform/Sleep.roc b/platform/Sleep.roc index 68f97966..f4356cc5 100644 --- a/platform/Sleep.roc +++ b/platform/Sleep.roc @@ -1,12 +1,7 @@ -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 => {} + millis! = |milliseconds| Host.sleep_millis!(milliseconds) +} diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index 3f47a612..7aa9dae5 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -1,802 +1,466 @@ -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 +import Host -## 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 : Host.SqliteStmt + + ## 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 }| + Host.sqlite_prepare!(path, q) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + + ## Bind named parameters to a prepared statement. + bind! = |stmt, bindings| + Host.sqlite_bind!(stmt, bindings) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + + ## Return the column names for a prepared statement. + columns! = |stmt| + Host.sqlite_columns!(stmt) + + ## Read the value of a column (by index) from the current row. + column_value! = |stmt, i| + Host.sqlite_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 Host.sqlite_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| + Host.sqlite_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 }) + } + + ## 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 {} [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 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! = |{ 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 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 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, + + # 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! = |{ 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_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!([]) } - => 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( + + # ---- 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| - 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 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| - decode! = gen_decode(cols) + |stmt| lookup_value!(cols, stmt, name) + ## Decode a column to a `Str`. + 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(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 an `I64`. + i64 = |name| int_decoder(name, |n| Ok(n)) + + ## Decode a column to an `I32`. + i32 = |name| int_decoder(name, |n| bounds_err(I64.to_i32_try(n))) + + ## Decode a column to an `I16`. + i16 = |name| int_decoder(name, |n| bounds_err(I64.to_i16_try(n))) + + ## Decode a column to an `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 an `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| + 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| + |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| - 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(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| + match lookup_value!(cols, stmt, name) { + Ok(Real(r)) => cast(r) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + + ## Convert an `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..2668485f 100644 --- a/platform/Stderr.roc +++ b/platform/Stderr.roc @@ -1,72 +1,27 @@ -module [ - IOErr, - line!, - write!, - write_bytes!, -] - +import IOErr exposing [IOErr] import Host -import InternalIOErr - -## **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, -] - -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) +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), ..]) + line! = |message| Ok(Host.stderr_line!(message)?) -## 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 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), ..]) + write! = |message| Ok(Host.stderr_write!(message)?) -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), ..]) + write_bytes! = |bytes| Ok(Host.stderr_write_bytes!(bytes)?) +} diff --git a/platform/Stdin.roc b/platform/Stdin.roc index 90ad5be6..93d29171 100644 --- a/platform/Stdin.roc +++ b/platform/Stdin.roc @@ -1,89 +1,27 @@ -module [ - IOErr, - line!, - bytes!, - read_to_end!, -] - +import IOErr exposing [IOErr] import Host -import InternalIOErr - -## **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, -] - -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) +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), ..]) + line! = || Ok(Host.stdin_line!()?) -## 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 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 combination 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), ..]) + bytes! = || Ok(Host.stdin_bytes!()?) -## Read all bytes from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)) -## until [EOF](https://en.wikipedia.org/wiki/End-of-file) in this source. -read_to_end! : {} => 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), ..]) + read_to_end! = || Ok(Host.stdin_read_to_end!()?) +} diff --git a/platform/Stdout.roc b/platform/Stdout.roc index 569299d2..a5a1c5c5 100644 --- a/platform/Stdout.roc +++ b/platform/Stdout.roc @@ -1,73 +1,27 @@ -module [ - IOErr, - line!, - write!, - write_bytes!, -] - +import IOErr exposing [IOErr] import Host -import InternalIOErr - -## **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, -] - -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) +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), ..]) + line! = |message| Ok(Host.stdout_line!(message)?) -## 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 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), ..]) + write! = |message| Ok(Host.stdout_write!(message)?) -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), ..]) + write_bytes! = |bytes| Ok(Host.stdout_write_bytes!(bytes)?) +} diff --git a/platform/Tcp.roc b/platform/Tcp.roc index 724126d1..98ea4460 100644 --- a/platform/Tcp.roc +++ b/platform/Tcp.roc @@ -1,225 +1,170 @@ -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 +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 : Host.TcpStream + + ## 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| + Host.tcp_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| + Host.tcp_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 Host.tcp_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| + Host.tcp_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| + Host.tcp_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) ----------------------------------------- -## 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 _ 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, -] + 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 : Str -> StreamErr 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..57f68f9e 100644 --- a/platform/Tty.roc +++ b/platform/Tty.roc @@ -1,26 +1,18 @@ -## 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!, -] - 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!({}) +## Provides functionality to change the behaviour of the terminal. +## This is useful for running an app like vim or a game in the terminal. +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! : () => {} + enable_raw_mode! = || Host.tty_enable_raw_mode!() -## Revert terminal to default behaviour -## -disable_raw_mode! : {} => {} -disable_raw_mode! = |{}| - Host.tty_mode_canonical!({}) + ## Revert terminal to default behaviour + disable_raw_mode! : () => {} + disable_raw_mode! = || Host.tty_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..fc301195 100644 --- a/platform/Utc.roc +++ b/platform/Utc.roc @@ -1,79 +1,26 @@ -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 + now! = || Host.utc_now!() + + ## 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..c7399481 100644 --- a/platform/main.roc +++ b/platform/main.roc @@ -1,69 +1,112 @@ -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, - ] - packages {} - imports [] - provides [main_for_host!] +platform "" + requires {} { main! : List(Str) => Try({}, [Exit(I32), ..]) } + exposes [Cmd, Dir, Env, File, Http, IOErr, InternalSqlite, Locale, Path, Random, Sleep, Sqlite, Stdin, Stdout, Stderr, Tcp, Tty, Utc] + packages { + # HTTP data types (Method, Request, Response) come from the shared + # roc-lang/http package so apps and other packages using it see the same + # nominal types. The platform supplies only the effectful `Http.send!`. + http: "https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst", + # Path data types and pure helpers come from the shared roc-lang/path + # package so path values have the same nominal type across packages. + path: "https://github.com/roc-lang/path/releases/download/1.0.0/8p8iryUUorAFTUDeqYcwc9bFYSwpbVqhYpuHvRAS5Cq4.tar.zst", + } + provides { "roc_main": main_for_host! } + hosted { + "hosted_cmd_host_exec_exit_code": Host.cmd_exec_exit_code!, + "hosted_cmd_host_exec_output": Host.cmd_exec_output!, + "hosted_dir_create": Host.dir_create!, + "hosted_dir_create_all": Host.dir_create_all!, + "hosted_dir_delete_all": Host.dir_delete_all!, + "hosted_dir_delete_empty": Host.dir_delete_empty!, + "hosted_dir_list": Host.dir_list!, + "hosted_env_cwd": Host.env_cwd!, + "hosted_env_exe_path": Host.env_exe_path!, + "hosted_env_temp_dir": Host.env_temp_dir!, + "hosted_env_var": Host.env_var!, + "hosted_file_delete": Host.file_delete!, + "hosted_file_is_executable": Host.file_is_executable!, + "hosted_file_is_readable": Host.file_is_readable!, + "hosted_file_is_writable": Host.file_is_writable!, + "hosted_file_read_bytes": Host.file_read_bytes!, + "hosted_file_read_utf8": Host.file_read_utf8!, + "hosted_file_open_reader": Host.file_open_reader!, + "hosted_file_read_line": Host.file_read_line!, + "hosted_file_size_in_bytes": Host.file_size_in_bytes!, + "hosted_file_time_accessed": Host.file_time_accessed!, + "hosted_file_time_created": Host.file_time_created!, + "hosted_file_time_modified": Host.file_time_modified!, + "hosted_file_write_bytes": Host.file_write_bytes!, + "hosted_file_write_utf8": Host.file_write_utf8!, + "hosted_locale_all": Host.locale_all!, + "hosted_locale_get": Host.locale_get!, + "hosted_path_type": Host.path_type!, + "hosted_random_seed_u32": Host.random_seed_u32!, + "hosted_random_seed_u64": Host.random_seed_u64!, + "hosted_sleep_millis": Host.sleep_millis!, + "hosted_stderr_line": Host.stderr_line!, + "hosted_stderr_write": Host.stderr_write!, + "hosted_stderr_write_bytes": Host.stderr_write_bytes!, + "hosted_stdin_bytes": Host.stdin_bytes!, + "hosted_stdin_line": Host.stdin_line!, + "hosted_stdin_read_to_end": Host.stdin_read_to_end!, + "hosted_stdout_line": Host.stdout_line!, + "hosted_stdout_write": Host.stdout_write!, + "hosted_stdout_write_bytes": Host.stdout_write_bytes!, + "hosted_tty_disable_raw_mode": Host.tty_disable_raw_mode!, + "hosted_tty_enable_raw_mode": Host.tty_enable_raw_mode!, + "hosted_utc_now": Host.utc_now!, + # SQLite hosted functions are kept at the end so adding them does not + # renumber the generated glue types for the modules declared above. + "hosted_sqlite_prepare": Host.sqlite_prepare!, + "hosted_sqlite_bind": Host.sqlite_bind!, + "hosted_sqlite_columns": Host.sqlite_columns!, + "hosted_sqlite_column_value": Host.sqlite_column_value!, + "hosted_sqlite_step": Host.sqlite_step!, + "hosted_sqlite_reset": Host.sqlite_reset!, + # TCP hosted functions are likewise kept at the end to avoid renumbering. + "hosted_tcp_connect": Host.tcp_connect!, + "hosted_tcp_read_up_to": Host.tcp_read_up_to!, + "hosted_tcp_read_exactly": Host.tcp_read_exactly!, + "hosted_tcp_read_until": Host.tcp_read_until!, + "hosted_tcp_write": Host.tcp_write!, + # HTTP is likewise kept at the end to avoid renumbering glue types. + "hosted_http_send_request": Host.http_send_request!, + } + 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 Host +import Http +import IOErr +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) +main_for_host! : List(Str) => I32 +main_for_host! = |args| + match main!(args) { + Ok({}) => 0 + Err(Exit(code)) => code + Err(other) => { + Stderr.line!("Program exited with error: ${Str.inspect(other)}") ?? {} 1 + } + } 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..29fb276a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2648 @@ +//! 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. `HostCmdExecExitCodeResult`), which are keyed by +// module + function name and therefore stable. Where our preferred local name is +// identical to a generated semantic alias (e.g. `HostIOErr`, `HostDirListResult`), we +// omit it here and rely on the `use crate::roc_platform_abi::*;` glob above. +type CmdExitResult = HostCmdExecExitCodeResult; +type CmdExitResultPayload = HostCmdExecExitCodeResultPayload; +type CmdExitResultTag = HostCmdExecExitCodeResultTag; +type CmdOutputResult = HostCmdExecOutputResult; +type CmdOutputResultPayload = HostCmdExecOutputResultPayload; +type CmdOutputResultTag = HostCmdExecOutputResultTag; +type CmdOutputFailureResult = HostCmdExecOutputErrResult; +type CmdOutputFailureResultPayload = HostCmdExecOutputErrResultPayload; +type CmdOutputFailureResultTag = HostCmdExecOutputErrResultTag; +type CmdOutputFailure = HostCmdExecOutputErrOk; +type CmdOutputSuccess = HostCmdExecOutputOk; +type Cmd = HostCmdExecExitCodeArgs; + +type DirUnitResult = HostDirCreateResult; +type DirUnitResultPayload = HostDirCreateResultPayload; +type DirUnitResultTag = HostDirCreateResultTag; + +type FileBytesResult = HostFileReadBytesResult; +type FileBytesResultPayload = HostFileReadBytesResultPayload; +type FileBytesResultTag = HostFileReadBytesResultTag; +type FileReaderOpenResult = HostFileOpenReaderResult; +type FileReaderOpenResultPayload = HostFileOpenReaderResultPayload; +type FileReaderOpenResultTag = HostFileOpenReaderResultTag; +type FileReaderLineResult = HostFileReadLineResult; +type FileReaderLineResultPayload = HostFileReadLineResultPayload; +type FileReaderLineResultTag = HostFileReadLineResultTag; +type FileStrResult = HostFileReadUtf8Result; +type FileStrResultPayload = HostFileReadUtf8ResultPayload; +type FileStrResultTag = HostFileReadUtf8ResultTag; +type FileSizeResult = HostFileSizeInBytesResult; +type FileSizeResultPayload = HostFileSizeInBytesResultPayload; +type FileSizeResultTag = HostFileSizeInBytesResultTag; +type FileBoolResult = HostFileIsExecutableResult; +type FileBoolResultPayload = HostFileIsExecutableResultPayload; +type FileBoolResultTag = HostFileIsExecutableResultTag; +type FileTimeResult = HostFileTimeAccessedResult; +type FileTimeResultPayload = HostFileTimeAccessedResultPayload; +type FileTimeResultTag = HostFileTimeAccessedResultTag; + +type RandomU64Result = HostRandomSeedU64Result; +type RandomU64ResultPayload = HostRandomSeedU64ResultPayload; +type RandomU64ResultTag = HostRandomSeedU64ResultTag; +type RandomU32Result = HostRandomSeedU32Result; +type RandomU32ResultPayload = HostRandomSeedU32ResultPayload; +type RandomU32ResultTag = HostRandomSeedU32ResultTag; + +type StderrUnitResult = HostStderrLineResult; +type StderrUnitResultPayload = HostStderrLineResultPayload; +type StderrUnitResultTag = HostStderrLineResultTag; +type StderrBytesResult = HostStderrWriteBytesResult; +type StderrBytesResultPayload = HostStderrWriteBytesResultPayload; +type StderrBytesResultTag = HostStderrWriteBytesResultTag; + +type StdinLineReadErr = EndOfFileOrStdinErr; +type StdinLineReadErrPayload = EndOfFileOrStdinErrPayload; +type StdinLineReadErrTag = EndOfFileOrStdinErrTag; +type StdinBytesReadErr = EndOfFileOrStdinErr; +type StdinBytesReadErrPayload = EndOfFileOrStdinErrPayload; +type StdinBytesReadErrTag = EndOfFileOrStdinErrTag; + +type StdoutUnitResult = HostStdoutLineResult; +type StdoutUnitResultPayload = HostStdoutLineResultPayload; +type StdoutUnitResultTag = HostStdoutLineResultTag; +type StdoutBytesResult = HostStdoutWriteBytesResult; +type StdoutBytesResultPayload = HostStdoutWriteBytesResultPayload; +type StdoutBytesResultTag = HostStdoutWriteBytesResultTag; + +fn roc_u8_list_from_slice(slice: &[u8], roc_host: &RocHost) -> RocListWith { + unsafe { RocListWith::::from_slice(slice, roc_host) } +} + +// ============================================================================ +// 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 = HostSqlitePrepareErr; +type SqliteBindings = AnonStruct56; + +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 = unsafe { + 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) { + unsafe { + decref_box_with( + handle as RocBox, + SQLITE_STMT_BOX_ALIGN, + 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) -> HostSqlitePrepareResult { + HostSqlitePrepareResult { + payload: HostSqlitePrepareResultPayload { + ok: ManuallyDrop::new(handle), + }, + tag: HostSqlitePrepareResultTag::Ok, + } +} + +fn try_sqlite_prepare_err(error: SqliteError) -> HostSqlitePrepareResult { + HostSqlitePrepareResult { + payload: HostSqlitePrepareResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostSqlitePrepareResultTag::Err, + } +} + +fn try_sqlite_unit_ok() -> HostSqliteBindResult { + HostSqliteBindResult { + payload: HostSqliteBindResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: HostSqliteBindResultTag::Ok, + } +} + +fn try_sqlite_unit_err(error: SqliteError) -> HostSqliteBindResult { + HostSqliteBindResult { + payload: HostSqliteBindResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostSqliteBindResultTag::Err, + } +} + +fn try_sqlite_value_ok(value: SqliteValue) -> HostSqliteColumnValueResult { + HostSqliteColumnValueResult { + payload: HostSqliteColumnValueResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostSqliteColumnValueResultTag::Ok, + } +} + +fn try_sqlite_value_err(error: SqliteError) -> HostSqliteColumnValueResult { + HostSqliteColumnValueResult { + payload: HostSqliteColumnValueResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostSqliteColumnValueResultTag::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) -> HostSqliteStepResult { + HostSqliteStepResult { + payload: HostSqliteStepResultPayload { + ok: ManuallyDrop::new(has_row), + }, + tag: HostSqliteStepResultTag::Ok, + } +} + +fn try_sqlite_step_err(error: SqliteError) -> HostSqliteStepResult { + HostSqliteStepResult { + payload: HostSqliteStepResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostSqliteStepResultTag::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, +) -> HostSqliteBindResult { + // 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) -> HostSqlitePrepareResult { + let roc_host = roc_host(); + let path_string = path.as_str().to_owned(); + let query_string = query.as_str().to_owned(); + unsafe { + path.decref(roc_host); + } + unsafe { + 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, +) -> HostSqliteBindResult { + 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_struct56(*binding, roc_host); + } + unsafe { + 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 = unsafe { 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, +) -> HostSqliteColumnValueResult { + 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(roc_u8_list_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) -> HostSqliteStepResult { + 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) -> HostSqliteBindResult { + 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 = unsafe { + 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) { + unsafe { + decref_box_with( + handle as RocBox, + TCP_STREAM_BOX_ALIGN, + 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) -> HostTcpConnectResult { + HostTcpConnectResult { + payload: HostTcpConnectResultPayload { + ok: ManuallyDrop::new(handle), + }, + tag: HostTcpConnectResultTag::Ok, + } +} + +fn try_tcp_connect_err(error: RocStr) -> HostTcpConnectResult { + HostTcpConnectResult { + payload: HostTcpConnectResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostTcpConnectResultTag::Err, + } +} + +// The three read host fns share an identical result layout (`Try(List U8, Str)`). +fn try_tcp_read_ok(bytes: RocListWith) -> HostTcpReadUpToResult { + HostTcpReadUpToResult { + payload: HostTcpReadUpToResultPayload { + ok: ManuallyDrop::new(bytes), + }, + tag: HostTcpReadUpToResultTag::Ok, + } +} + +fn try_tcp_read_err(error: RocStr) -> HostTcpReadUpToResult { + HostTcpReadUpToResult { + payload: HostTcpReadUpToResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostTcpReadUpToResultTag::Err, + } +} + +fn try_tcp_write_ok() -> HostTcpWriteResult { + HostTcpWriteResult { + payload: HostTcpWriteResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: HostTcpWriteResultTag::Ok, + } +} + +fn try_tcp_write_err(error: RocStr) -> HostTcpWriteResult { + HostTcpWriteResult { + payload: HostTcpWriteResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostTcpWriteResultTag::Err, + } +} + +#[no_mangle] +pub extern "C" fn hosted_tcp_connect(host: RocStr, port: u16) -> HostTcpConnectResult { + let roc_host = roc_host(); + let host_string = host.as_str().to_owned(); + unsafe { + 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, +) -> HostTcpReadUpToResult { + 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(roc_u8_list_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, +) -> HostTcpReadExactlyResult { + 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(roc_u8_list_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) -> HostTcpReadUntilResult { + 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(roc_u8_list_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, +) -> HostTcpWriteResult { + 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)), + } + }; + unsafe { + 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 embedded WebPKI root certificates. +// +// Transport failures are surfaced to Roc as a typed `Try` error, separate from +// real HTTP responses. This avoids interpreting a legitimate server status/body +// pair as a platform transport failure. +// ---------------------------------------------------------------------------- + +// The generated glue names the request/response records by anonymous-struct +// number; alias them to stable semantic names. Host ABI headers are `(Str, Str)` +// tuples, rendered as a struct with `_0` (name) and `_1` (value) fields. +type HttpResponse = HostHttpSendRequestOk; +type HttpHeader = AnonStruct39; +type HttpResult = HostHttpSendRequestResult; +type HttpResultPayload = HostHttpSendRequestResultPayload; +type HttpResultTag = HostHttpSendRequestResultTag; +type HttpTransportErr = BadBodyOrNetworkErrorOrOtherOrTimeout; +type HttpTransportErrPayload = BadBodyOrNetworkErrorOrOtherOrTimeoutPayload; +type HttpTransportErrTag = BadBodyOrNetworkErrorOrOtherOrTimeoutTag; + +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/InternalHttp.roc. +fn as_hyper_method(method: u8, 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_ok(response: HttpResponse) -> HttpResult { + HttpResult { + payload: HttpResultPayload { + ok: ManuallyDrop::new(response), + }, + tag: HttpResultTag::Ok, + } +} + +fn http_err(error: HttpTransportErr) -> HttpResult { + HttpResult { + payload: HttpResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HttpResultTag::Err, + } +} + +fn http_err_timeout() -> HttpTransportErr { + HttpTransportErr { + payload: HttpTransportErrPayload { timeout: [] }, + tag: HttpTransportErrTag::Timeout, + } +} + +fn http_err_bad_body() -> HttpTransportErr { + HttpTransportErr { + payload: HttpTransportErrPayload { bad_body: [] }, + tag: HttpTransportErrTag::BadBody, + } +} + +fn http_err_other(message: &str, roc_host: &RocHost) -> HttpTransportErr { + HttpTransportErr { + payload: HttpTransportErrPayload { + other: ManuallyDrop::new(roc_u8_list_from_slice(message.as_bytes(), roc_host)), + }, + tag: HttpTransportErrTag::Other, + } +} + +fn build_hyper_request( + args: &HostHttpSendRequestArgs, +) -> 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._0.as_str(), header._1.as_str()); + if header._0.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 = unsafe { RocList::::allocate(pairs.len(), roc_host) }; + for (index, (name, value)) in pairs.iter().enumerate() { + let header = HttpHeader { + _0: RocStr::from_str(name, roc_host), + _1: 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, +) -> HttpResult { + use http_body_util::BodyExt; + use hyper_rustls::HttpsConnectorBuilder; + use hyper_util::client::legacy::Client; + use hyper_util::rt::TokioExecutor; + + let https = HttpsConnectorBuilder::new() + .with_webpki_roots() + .https_or_http() + .enable_http1() + .build(); + + 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(); + http_ok(HttpResponse { + body: roc_u8_list_from_slice(&bytes, roc_host), + headers: build_roc_headers(&pairs, roc_host), + status, + }) + } + Err(_) => http_err(http_err_bad_body()), + } + } + Err(err) => { + let detail = err.to_string(); + http_err(http_err_other(&detail, roc_host)) + } + } +} + +#[no_mangle] +pub extern "C" fn hosted_http_send_request(args: HostHttpSendRequestArgs) -> HttpResult { + 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); + unsafe { + args.body.decref(roc_host); + } + for header in args.headers.as_slice() { + decref_anon_struct39(*header, roc_host); + } + unsafe { + args.headers.decref(roc_host); + } + unsafe { + args.method_ext.decref(roc_host); + } + unsafe { + args.uri.decref(roc_host); + } + + let request = match request_result { + Ok(request) => request, + Err(err) => return http_err(http_err_other(&err, 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_err(http_err_timeout()), + } + }) + } 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, + HostIOErr, + HostIOErrTag, + HostIOErrPayload +); +define_common_io_err!( + dir_io_err_from_io, + dir_io_err_other, + HostIOErr, + HostIOErrTag, + HostIOErrPayload +); +define_common_io_err!( + file_io_err_from_io, + file_io_err_other, + HostIOErr, + HostIOErrTag, + HostIOErrPayload +); +define_common_io_err!( + path_io_err_from_io, + path_io_err_other, + HostIOErr, + HostIOErrTag, + HostIOErrPayload +); +define_common_io_err!( + random_io_err_from_io, + random_io_err_other, + HostIOErr, + HostIOErrTag, + HostIOErrPayload +); +define_common_io_err!( + stderr_io_err_from_io, + stderr_io_err_other, + HostIOErr, + HostIOErrTag, + HostIOErrPayload +); +define_common_io_err!( + stdin_io_err_from_io, + stdin_io_err_other, + HostIOErr, + HostIOErrTag, + HostIOErrPayload +); +define_common_io_err!( + stdout_io_err_from_io, + stdout_io_err_other, + HostIOErr, + HostIOErrTag, + HostIOErrPayload +); + +fn decref_roc_str_list(list: &RocList, roc_host: &RocHost) { + for item in list.as_slice() { + unsafe { + item.decref(roc_host); + } + } + unsafe { + 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); + unsafe { + 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: HostIOErr) -> 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: HostIOErr) -> 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: HostIOErr) -> DirUnitResult { + DirUnitResult { + payload: DirUnitResultPayload { + err: ManuallyDrop::new(error), + }, + tag: DirUnitResultTag::Err, + } +} + +fn try_dir_list_ok(value: RocList>) -> HostDirListResult { + HostDirListResult { + payload: HostDirListResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostDirListResultTag::Ok, + } +} + +fn try_dir_list_err(error: HostIOErr) -> HostDirListResult { + HostDirListResult { + payload: HostDirListResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostDirListResultTag::Err, + } +} + +fn try_env_str_ok(value: RocStr) -> HostEnvVarResult { + HostEnvVarResult { + payload: HostEnvVarResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostEnvVarResultTag::Ok, + } +} + +fn try_env_str_err(error: RocStr) -> HostEnvVarResult { + HostEnvVarResult { + payload: HostEnvVarResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostEnvVarResultTag::Err, + } +} + +fn try_env_cwd_ok(value: RocListWith) -> HostEnvCwdResult { + HostEnvCwdResult { + payload: HostEnvCwdResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostEnvCwdResultTag::Ok, + } +} + +fn try_env_cwd_err() -> HostEnvCwdResult { + HostEnvCwdResult { + payload: HostEnvCwdResultPayload { + err: ManuallyDrop::new(core::ptr::null_mut()), + }, + tag: HostEnvCwdResultTag::Err, + } +} + +fn try_env_exe_path_ok(value: RocListWith) -> HostEnvExePathResult { + HostEnvExePathResult { + payload: HostEnvExePathResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostEnvExePathResultTag::Ok, + } +} + +fn try_env_exe_path_err() -> HostEnvExePathResult { + HostEnvExePathResult { + payload: HostEnvExePathResultPayload { + err: ManuallyDrop::new(core::ptr::null_mut()), + }, + tag: HostEnvExePathResultTag::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: HostIOErr) -> FileBytesResult { + FileBytesResult { + payload: FileBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileBytesResultTag::Err, + } +} + +fn try_file_reader_ok(handle: *mut u64) -> FileReaderOpenResult { + FileReaderOpenResult { + payload: FileReaderOpenResultPayload { + ok: ManuallyDrop::new(handle), + }, + tag: FileReaderOpenResultTag::Ok, + } +} + +fn try_file_reader_err(error: HostIOErr) -> FileReaderOpenResult { + FileReaderOpenResult { + payload: FileReaderOpenResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileReaderOpenResultTag::Err, + } +} + +fn try_file_reader_line_ok(value: RocListWith) -> FileReaderLineResult { + FileReaderLineResult { + payload: FileReaderLineResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: FileReaderLineResultTag::Ok, + } +} + +fn try_file_reader_line_err(error: HostIOErr) -> FileReaderLineResult { + FileReaderLineResult { + payload: FileReaderLineResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileReaderLineResultTag::Err, + } +} + +fn try_file_write_bytes_ok() -> HostFileWriteBytesResult { + HostFileWriteBytesResult { + payload: HostFileWriteBytesResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: HostFileWriteBytesResultTag::Ok, + } +} + +fn try_file_write_bytes_err(error: HostIOErr) -> HostFileWriteBytesResult { + HostFileWriteBytesResult { + payload: HostFileWriteBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostFileWriteBytesResultTag::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: HostIOErr) -> FileStrResult { + FileStrResult { + payload: FileStrResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileStrResultTag::Err, + } +} + +fn try_file_write_utf8_ok() -> HostFileWriteUtf8Result { + HostFileWriteUtf8Result { + payload: HostFileWriteUtf8ResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: HostFileWriteUtf8ResultTag::Ok, + } +} + +fn try_file_write_utf8_err(error: HostIOErr) -> HostFileWriteUtf8Result { + HostFileWriteUtf8Result { + payload: HostFileWriteUtf8ResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostFileWriteUtf8ResultTag::Err, + } +} + +fn try_file_delete_ok() -> HostFileDeleteResult { + HostFileDeleteResult { + payload: HostFileDeleteResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: HostFileDeleteResultTag::Ok, + } +} + +fn try_file_delete_err(error: HostIOErr) -> HostFileDeleteResult { + HostFileDeleteResult { + payload: HostFileDeleteResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostFileDeleteResultTag::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: HostIOErr) -> 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: HostIOErr) -> 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: HostIOErr) -> FileTimeResult { + FileTimeResult { + payload: FileTimeResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileTimeResultTag::Err, + } +} + +fn try_locale_get_ok(value: RocStr) -> HostLocaleGetResult { + HostLocaleGetResult { + payload: HostLocaleGetResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostLocaleGetResultTag::Ok, + } +} + +fn try_path_type_ok(value: HostPathTypeOk) -> HostPathTypeResult { + HostPathTypeResult { + payload: HostPathTypeResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostPathTypeResultTag::Ok, + } +} + +fn try_path_type_err(error: HostIOErr) -> HostPathTypeResult { + HostPathTypeResult { + payload: HostPathTypeResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostPathTypeResultTag::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: HostIOErr) -> 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: HostIOErr) -> 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: HostIOErr) -> 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: HostIOErr) -> 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: HostIOErr) -> 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: HostIOErr) -> StdinBytesReadErr { + StdinBytesReadErr { + payload: StdinBytesReadErrPayload { + stdin_err: ManuallyDrop::new(error), + }, + tag: StdinBytesReadErrTag::StdinErr, + } +} + +fn try_stdin_line_ok(value: RocStr) -> HostStdinLineResult { + HostStdinLineResult { + payload: HostStdinLineResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostStdinLineResultTag::Ok, + } +} + +fn try_stdin_line_err(error: StdinLineReadErr) -> HostStdinLineResult { + HostStdinLineResult { + payload: HostStdinLineResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostStdinLineResultTag::Err, + } +} + +fn try_stdin_bytes_ok(value: RocListWith) -> HostStdinBytesResult { + HostStdinBytesResult { + payload: HostStdinBytesResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostStdinBytesResultTag::Ok, + } +} + +fn try_stdin_bytes_err(error: StdinBytesReadErr) -> HostStdinBytesResult { + HostStdinBytesResult { + payload: HostStdinBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostStdinBytesResultTag::Err, + } +} + +fn try_stdin_read_to_end_ok(value: RocListWith) -> HostStdinReadToEndResult { + HostStdinReadToEndResult { + payload: HostStdinReadToEndResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostStdinReadToEndResultTag::Ok, + } +} + +fn try_stdin_read_to_end_err(error: HostIOErr) -> HostStdinReadToEndResult { + HostStdinReadToEndResult { + payload: HostStdinReadToEndResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostStdinReadToEndResultTag::Err, + } +} + +fn try_stdout_unit_ok() -> StdoutUnitResult { + StdoutUnitResult { + payload: StdoutUnitResultPayload { + ok: ManuallyDrop::new(()), + }, + tag: StdoutUnitResultTag::Ok, + } +} + +fn try_stdout_unit_err(error: HostIOErr) -> 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: HostIOErr) -> 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 = roc_u8_list_from_slice(&output.stdout, roc_host); + let stderr_bytes = roc_u8_list_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 => { + unsafe { + stdout_bytes.decref(roc_host); + } + unsafe { + 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(); + unsafe { + path.decref(roc_host); + } + path_string +} + +fn path_bytes_from_path(path: &std::path::Path, roc_host: &RocHost) -> RocListWith { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + roc_u8_list_from_slice(path.as_os_str().as_bytes(), roc_host) + } + + #[cfg(not(unix))] + { + roc_u8_list_from_slice(path.to_string_lossy().as_bytes(), roc_host) + } +} + +#[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) -> HostDirListResult { + 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())) + .collect(); + let list = + unsafe { RocList::>::allocate(entries.len(), roc_host) }; + for (index, entry) in entries.iter().enumerate() { + unsafe { + list.elements + .add(index) + .write(path_bytes_from_path(entry.as_path(), 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() -> HostEnvCwdResult { + let roc_host = roc_host(); + match std::env::current_dir() { + Ok(path) => try_env_cwd_ok(path_bytes_from_path(path.as_path(), roc_host)), + Err(_) => try_env_cwd_err(), + } +} + +#[no_mangle] +pub extern "C" fn hosted_env_exe_path() -> HostEnvExePathResult { + let roc_host = roc_host(); + match std::env::current_exe() { + Ok(path) => try_env_exe_path_ok(path_bytes_from_path(path.as_path(), roc_host)), + Err(_) => try_env_exe_path_err(), + } +} + +#[no_mangle] +pub extern "C" fn hosted_env_temp_dir() -> RocListWith { + let roc_host = roc_host(); + path_bytes_from_path(std::env::temp_dir().as_path(), roc_host) +} + +#[no_mangle] +pub extern "C" fn hosted_env_var(name: RocStr) -> HostEnvVarResult { + let roc_host = roc_host(); + let key = name.as_str().to_owned(); + match std::env::var_os(&key) { + Some(value) => { + unsafe { + 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) -> HostFileDeleteResult { + 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(roc_u8_list_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)), + } +} + +// ============================================================================ +// Buffered file readers +// +// `File.Reader` (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 file happens in +// `drop_file_reader` when the last reference is released. +// ---------------------------------------------------------------------------- + +const FILE_READER_BOX_ALIGN: usize = core::mem::align_of::(); + +fn box_file_reader(reader: BufReader, roc_host: &RocHost) -> *mut u64 { + let raw: *mut BufReader = Box::into_raw(Box::new(reader)); + let boxed = unsafe { + allocate_box( + core::mem::size_of::(), + FILE_READER_BOX_ALIGN, + false, + roc_host, + ) + }; + unsafe { + *(boxed as *mut u64) = raw as u64; + } + boxed as *mut u64 +} + +unsafe fn file_reader_ref<'a>(handle: *mut u64) -> &'a mut BufReader { + &mut *(*handle as *mut BufReader) +} + +extern "C" fn drop_file_reader(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_file_reader(handle: *mut u64, roc_host: &RocHost) { + unsafe { + decref_box_with( + handle as RocBox, + FILE_READER_BOX_ALIGN, + false, + Some(drop_file_reader), + roc_host, + ); + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_open_reader(path: RocStr, capacity: u64) -> FileReaderOpenResult { + let roc_host = roc_host(); + match fs::File::open(path_from_roc_str(path, roc_host)) { + Ok(file) => { + let reader = if capacity == 0 { + BufReader::new(file) + } else { + BufReader::with_capacity(capacity as usize, file) + }; + try_file_reader_ok(box_file_reader(reader, roc_host)) + } + Err(error) => try_file_reader_err(file_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_read_line(handle: *mut u64) -> FileReaderLineResult { + let roc_host = roc_host(); + let result = { + let reader = unsafe { file_reader_ref(handle) }; + let mut buffer = Vec::new(); + match reader.read_until(b'\n', &mut buffer) { + Ok(_) => try_file_reader_line_ok(roc_u8_list_from_slice(&buffer, roc_host)), + Err(error) => try_file_reader_line_err(file_io_err_from_io(&error, roc_host)), + } + }; + release_file_reader(handle, roc_host); + result +} + +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, +) -> HostFileWriteBytesResult { + let roc_host = roc_host(); + let path_string = path_from_roc_str(path, roc_host); + let result = fs::write(path_string, bytes.as_slice()); + unsafe { + 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) -> HostFileWriteUtf8Result { + let roc_host = roc_host(); + let path_string = path_from_roc_str(path, roc_host); + let content_string = content.as_str().to_owned(); + unsafe { + 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 = unsafe { 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() -> HostLocaleGetResult { + let roc_host = roc_host(); + try_locale_get_ok(RocStr::from_str(&locale_get_string(), roc_host)) +} + +fn path_buf_from_bytes(path: RocListWith, roc_host: &RocHost) -> std::path::PathBuf { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + + let path_buf = std::path::PathBuf::from(std::ffi::OsStr::from_bytes(path.as_slice())); + unsafe { + path.decref(roc_host); + } + path_buf + } + + #[cfg(not(unix))] + { + let path_buf = std::path::PathBuf::from(String::from_utf8_lossy(path.as_slice()).as_ref()); + unsafe { + path.decref(roc_host); + } + path_buf + } +} + +#[no_mangle] +pub extern "C" fn hosted_path_type(path: RocListWith) -> HostPathTypeResult { + let roc_host = roc_host(); + let path = path_buf_from_bytes(path, roc_host); + + match path.symlink_metadata() { + Ok(metadata) => { + let file_type = metadata.file_type(); + try_path_type_ok(HostPathTypeOk { + 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()) + }; + unsafe { + 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()) + }; + unsafe { + 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()) + }; + unsafe { + 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() -> HostStdinLineResult { + 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() -> HostStdinBytesResult { + 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(roc_u8_list_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() -> HostStdinReadToEndResult { + 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(roc_u8_list_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()) + }; + unsafe { + 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()) + }; + unsafe { + 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()) + }; + unsafe { + 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 = unsafe { 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..df847ba9 --- /dev/null +++ b/src/roc_platform_abi.rs @@ -0,0 +1,3929 @@ +//! 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 __AnonStruct4 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct4 { + pub args: RocList, + pub envs: RocList, + pub program: RocStr, + pub clear_envs: bool, +} + +const _: () = assert!(core::mem::size_of::() == 80, "AnonStruct4 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct4 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 __AnonStruct37 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct37 { + pub body: RocListWith, + pub headers: RocList, + pub status: u16, +} + +const _: () = assert!(core::mem::size_of::() == 56, "AnonStruct37 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct37 alignment mismatch"); + +/// Element type for __AnonStruct39 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct39 { + pub _0: RocStr, + pub _1: RocStr, +} + +const _: () = assert!(core::mem::size_of::() == 48, "AnonStruct39 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct39 alignment mismatch"); + +/// Element type for __AnonStruct41 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct41 { + pub timeout_ms: u64, + pub body: RocListWith, + pub headers: RocList, + pub method_ext: RocStr, + pub uri: RocStr, + pub method: u8, +} + +const _: () = assert!(core::mem::size_of::() == 112, "AnonStruct41 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct41 alignment mismatch"); + +/// Element type for __AnonStruct45 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct45 { + pub is_dir: bool, + pub is_file: bool, + pub is_sym_link: bool, +} + +const _: () = assert!(core::mem::size_of::() == 3, "AnonStruct45 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "AnonStruct45 alignment mismatch"); + +/// Element type for __AnonStruct51 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct51 { + pub code: i64, + pub message: RocStr, +} + +const _: () = assert!(core::mem::size_of::() == 32, "AnonStruct51 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct51 alignment mismatch"); + +/// Element type for __AnonStruct56 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct56 { + pub value: BytesOrIntegerOrNullOrRealOrString, + pub name: RocStr, +} + +const _: () = assert!(core::mem::size_of::() == 56, "AnonStruct56 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct56 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 IOErrTag { + 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 IOErr { + pub payload: IOErrPayload, + pub tag: IOErrTag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrPayload { + 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, "IOErr size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "IOErr 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 Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType16Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType16 { + pub payload: TryType16Payload, + pub tag: TryType16Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType16Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType16 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType16 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::() == 32, "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 TryType20Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType20 { + pub payload: TryType20Payload, + pub tag: TryType20Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType20Payload { + pub err: core::mem::ManuallyDrop<*mut c_void>, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType20 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType20 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType22Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType22 { + pub payload: TryType22Payload, + pub tag: TryType22Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType22Payload { + pub err: core::mem::ManuallyDrop<*mut c_void>, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType22 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType22 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, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "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 TryType26Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType26 { + pub payload: TryType26Payload, + pub tag: TryType26Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType26Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType26 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType26 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, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "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 TryType28Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType28 { + pub payload: TryType28Payload, + pub tag: TryType28Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType28Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<*mut u64>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType28 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType28 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType31Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType31 { + pub payload: TryType31Payload, + pub tag: TryType31Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType31Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType31 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType31 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType32Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType32 { + pub payload: TryType32Payload, + pub tag: TryType32Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType32Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType32 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType32 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType33Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType33 { + pub payload: TryType33Payload, + pub tag: TryType33Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType33Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 48, "TryType33 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 16, "TryType33 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::() == 64, "TryType35 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType35 alignment mismatch"); + +/// Tag discriminant for BadBodyOrNetworkErrorOrOtherOrTimeout. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BadBodyOrNetworkErrorOrOtherOrTimeoutTag { + BadBody = 0, + NetworkError = 1, + Other = 2, + Timeout = 3, +} + +/// Tag union: BadBodyOrNetworkErrorOrOtherOrTimeout +#[repr(C)] +#[derive(Clone, Copy)] +pub struct BadBodyOrNetworkErrorOrOtherOrTimeout { + pub payload: BadBodyOrNetworkErrorOrOtherOrTimeoutPayload, + pub tag: BadBodyOrNetworkErrorOrOtherOrTimeoutTag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union BadBodyOrNetworkErrorOrOtherOrTimeoutPayload { + pub bad_body: [u8; 0], + pub network_error: [u8; 0], + pub other: core::mem::ManuallyDrop>, + pub timeout: [u8; 0], +} + +const _: () = assert!(core::mem::size_of::() == 32, "BadBodyOrNetworkErrorOrOtherOrTimeout size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "BadBodyOrNetworkErrorOrOtherOrTimeout 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<*mut c_void>, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 32, "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 TryType46Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType46 { + pub payload: TryType46Payload, + pub tag: TryType46Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType46Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType46 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType46 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType48Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType48 { + pub payload: TryType48Payload, + pub tag: TryType48Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType48Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType48 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType48 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<*mut u64>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType50 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType50 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType54Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType54 { + pub payload: TryType54Payload, + pub tag: TryType54Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType54Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType54 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType54 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 TryType59Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType59 { + pub payload: TryType59Payload, + pub tag: TryType59Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType59Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType59 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType59 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType60Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType60 { + pub payload: TryType60Payload, + pub tag: TryType60Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType60Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType60 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType60 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType61Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType61 { + pub payload: TryType61Payload, + pub tag: TryType61Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType61Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType61 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType61 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType63Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType63 { + pub payload: TryType63Payload, + pub tag: TryType63Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType63Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 48, "TryType63 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType63 alignment mismatch"); + +/// Tag discriminant for EndOfFileOrStdinErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EndOfFileOrStdinErrTag { + EndOfFile = 0, + StdinErr = 1, +} + +/// Tag union: EndOfFileOrStdinErr +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EndOfFileOrStdinErr { + pub payload: EndOfFileOrStdinErrPayload, + pub tag: EndOfFileOrStdinErrTag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union EndOfFileOrStdinErrPayload { + pub end_of_file: [u8; 0], + pub stdin_err: core::mem::ManuallyDrop, +} + +const _: () = assert!(core::mem::size_of::() == 40, "EndOfFileOrStdinErr size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "EndOfFileOrStdinErr alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType65Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType65 { + pub payload: TryType65Payload, + pub tag: TryType65Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType65Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 48, "TryType65 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType65 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 Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType68Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType68 { + pub payload: TryType68Payload, + pub tag: TryType68Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType68Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 40, "TryType68 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType68 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType70Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType70 { + pub payload: TryType70Payload, + pub tag: TryType70Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType70Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<*mut u64>, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType70 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType70 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType72Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType72 { + pub payload: TryType72Payload, + pub tag: TryType72Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType72Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +const _: () = assert!(core::mem::size_of::() == 32, "TryType72 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType72 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::() == 32, "TryType73 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TryType73 alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType77Tag { + Err = 0, + Ok = 1, +} + +/// Tag union: Try +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType77 { + pub payload: TryType77Payload, + pub tag: TryType77Tag, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType77Payload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<()>, +} + +const _: () = assert!(core::mem::size_of::() == 8, "TryType77 size mismatch"); +const _: () = assert!(core::mem::align_of::() == 4, "TryType77 alignment mismatch"); + +/// Arguments for Host.cmd_exec_exit_code! +/// Roc signature: { args : List(Str), clear_envs : Bool, envs : List(Str), program : Str } => Try(I32, IOErr) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecExitCodeArgs { + pub args: RocList, + pub envs: RocList, + pub program: RocStr, + pub clear_envs: bool, +} + +const _: () = assert!(core::mem::size_of::() == 80, "HostCmdExecExitCodeArgs size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "HostCmdExecExitCodeArgs alignment mismatch"); + +/// Arguments for Host.cmd_exec_output! +/// Roc signature: { args : List(Str), clear_envs : Bool, envs : List(Str), program : Str } => 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 HostCmdExecOutputArgs { + pub args: RocList, + pub envs: RocList, + pub program: RocStr, + pub clear_envs: bool, +} + +const _: () = assert!(core::mem::size_of::() == 80, "HostCmdExecOutputArgs size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "HostCmdExecOutputArgs alignment mismatch"); + +/// Arguments for Host.dir_create! +/// Roc signature: Str => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirCreateArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.dir_create_all! +/// Roc signature: Str => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirCreateAllArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.dir_delete_all! +/// Roc signature: Str => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirDeleteAllArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.dir_delete_empty! +/// Roc signature: Str => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirDeleteEmptyArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.dir_list! +/// Roc signature: Str => Try(List(List(U8)), [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirListArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.env_var! +/// Roc signature: Str => Try(Str, [VarNotFound(Str)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvVarArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.file_delete! +/// Roc signature: Str => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileDeleteArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostFileIsExecutableArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostFileIsReadableArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostFileIsWritableArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.file_open_reader! +/// Roc signature: Str, U64 => Try(Host.FileReader, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileOpenReaderArgs { + pub arg0: RocStr, + pub arg1: u64, +} + +/// Arguments for Host.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 HostFileReadBytesArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.file_read_line! +/// Roc signature: Host.FileReader => Try(List(U8), [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileReadLineArgs { + pub arg0: *mut u64, +} + +/// Arguments for Host.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 HostFileReadUtf8Args { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostFileSizeInBytesArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostFileTimeAccessedArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostFileTimeCreatedArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostFileTimeModifiedArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostFileWriteBytesArgs { + pub arg0: RocStr, + pub arg1: RocListWith, +} + +/// Arguments for Host.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 HostFileWriteUtf8Args { + pub arg0: RocStr, + pub arg1: RocStr, +} + +/// Arguments for Host.http_send_request! +/// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str } => Try({ body : List(U8), headers : List((Str, Str)), status : U16 }, [BadBody, NetworkError, Other(List(U8)), Timeout]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostHttpSendRequestArgs { + pub timeout_ms: u64, + pub body: RocListWith, + pub headers: RocList, + pub method_ext: RocStr, + pub uri: RocStr, + pub method: u8, +} + +const _: () = assert!(core::mem::size_of::() == 112, "HostHttpSendRequestArgs size mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "HostHttpSendRequestArgs alignment mismatch"); + +/// Arguments for 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 HostPathTypeArgs { + pub arg0: RocListWith, +} + +/// Arguments for Host.sleep_millis! +/// Roc signature: U64 => {} +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSleepMillisArgs { + pub arg0: u64, +} + +/// Arguments for Host.sqlite_bind! +/// Roc signature: Host.SqliteStmt, 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 HostSqliteBindArgs { + pub arg0: *mut u64, + pub arg1: RocList, +} + +/// Arguments for Host.sqlite_column_value! +/// Roc signature: Host.SqliteStmt, 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 HostSqliteColumnValueArgs { + pub arg0: *mut u64, + pub arg1: u64, +} + +/// Arguments for Host.sqlite_columns! +/// Roc signature: Host.SqliteStmt => List(Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteColumnsArgs { + pub arg0: *mut u64, +} + +/// Arguments for Host.sqlite_prepare! +/// Roc signature: Str, Str => Try(Host.SqliteStmt, { code : I64, message : Str }) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqlitePrepareArgs { + pub arg0: RocStr, + pub arg1: RocStr, +} + +/// Arguments for Host.sqlite_reset! +/// Roc signature: Host.SqliteStmt => Try({}, { code : I64, message : Str }) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteResetArgs { + pub arg0: *mut u64, +} + +/// Arguments for Host.sqlite_step! +/// Roc signature: Host.SqliteStmt => Try(Bool, { code : I64, message : Str }) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteStepArgs { + pub arg0: *mut u64, +} + +/// Arguments for Host.stderr_line! +/// Roc signature: Str => Try({}, [StderrErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStderrLineArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.stderr_write! +/// Roc signature: Str => Try({}, [StderrErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStderrWriteArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostStderrWriteBytesArgs { + pub arg0: RocListWith, +} + +/// Arguments for Host.stdout_line! +/// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdoutLineArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.stdout_write! +/// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdoutWriteArgs { + pub arg0: RocStr, +} + +/// Arguments for Host.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 HostStdoutWriteBytesArgs { + pub arg0: RocListWith, +} + +/// Arguments for Host.tcp_connect! +/// Roc signature: Str, U16 => Try(Host.TcpStream, Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpConnectArgs { + pub arg0: RocStr, + pub arg1: u16, +} + +/// Arguments for Host.tcp_read_exactly! +/// Roc signature: Host.TcpStream, U64 => Try(List(U8), Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpReadExactlyArgs { + pub arg0: *mut u64, + pub arg1: u64, +} + +/// Arguments for Host.tcp_read_until! +/// Roc signature: Host.TcpStream, U8 => Try(List(U8), Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpReadUntilArgs { + pub arg0: *mut u64, + pub arg1: u8, +} + +/// Arguments for Host.tcp_read_up_to! +/// Roc signature: Host.TcpStream, U64 => Try(List(U8), Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpReadUpToArgs { + pub arg0: *mut u64, + pub arg1: u64, +} + +/// Arguments for Host.tcp_write! +/// Roc signature: Host.TcpStream, List(U8) => Try({}, Str) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpWriteArgs { + pub arg0: *mut u64, + pub arg1: RocListWith, +} + +// ============================================================================= +// Semantic Type Aliases +// ============================================================================= + +pub type HostCmdExecExitCodeResult = TryType0; +pub type HostCmdExecExitCodeResultPayload = TryType0Payload; +pub type HostCmdExecExitCodeResultTag = TryType0Tag; +pub type HostIOErr = IOErr; +pub type HostIOErrPayload = IOErrPayload; +pub type HostIOErrTag = IOErrTag; +pub type HostCmdExecOutputResult = TryType7; +pub type HostCmdExecOutputResultPayload = TryType7Payload; +pub type HostCmdExecOutputResultTag = TryType7Tag; +pub type HostCmdExecOutputErrResult = TryType8; +pub type HostCmdExecOutputErrResultPayload = TryType8Payload; +pub type HostCmdExecOutputErrResultTag = TryType8Tag; +pub type HostCmdExecOutputErrOk = AnonStruct9; +pub type HostCmdExecOutputOk = AnonStruct12; +pub type HostDirCreateResult = TryType13; +pub type HostDirCreateResultPayload = TryType13Payload; +pub type HostDirCreateResultTag = TryType13Tag; +pub type HostDirCreateAllResult = TryType13; +pub type HostDirCreateAllResultPayload = TryType13Payload; +pub type HostDirCreateAllResultTag = TryType13Tag; +pub type HostDirDeleteAllResult = TryType13; +pub type HostDirDeleteAllResultPayload = TryType13Payload; +pub type HostDirDeleteAllResultTag = TryType13Tag; +pub type HostDirDeleteEmptyResult = TryType13; +pub type HostDirDeleteEmptyResultPayload = TryType13Payload; +pub type HostDirDeleteEmptyResultTag = TryType13Tag; +pub type HostDirListResult = TryType16; +pub type HostDirListResultPayload = TryType16Payload; +pub type HostDirListResultTag = TryType16Tag; +pub type HostEnvCwdResult = TryType20; +pub type HostEnvCwdResultPayload = TryType20Payload; +pub type HostEnvCwdResultTag = TryType20Tag; +pub type HostEnvExePathResult = TryType22; +pub type HostEnvExePathResultPayload = TryType22Payload; +pub type HostEnvExePathResultTag = TryType22Tag; +pub type HostEnvVarResult = TryType18; +pub type HostEnvVarResultPayload = TryType18Payload; +pub type HostEnvVarResultTag = TryType18Tag; +pub type HostFileDeleteResult = TryType26; +pub type HostFileDeleteResultPayload = TryType26Payload; +pub type HostFileDeleteResultTag = TryType26Tag; +pub type HostFileIsExecutableResult = TryType32; +pub type HostFileIsExecutableResultPayload = TryType32Payload; +pub type HostFileIsExecutableResultTag = TryType32Tag; +pub type HostFileIsReadableResult = TryType32; +pub type HostFileIsReadableResultPayload = TryType32Payload; +pub type HostFileIsReadableResultTag = TryType32Tag; +pub type HostFileIsWritableResult = TryType32; +pub type HostFileIsWritableResultPayload = TryType32Payload; +pub type HostFileIsWritableResultTag = TryType32Tag; +pub type HostFileOpenReaderResult = TryType28; +pub type HostFileOpenReaderResultPayload = TryType28Payload; +pub type HostFileOpenReaderResultTag = TryType28Tag; +pub type HostFileReadBytesResult = TryType24; +pub type HostFileReadBytesResultPayload = TryType24Payload; +pub type HostFileReadBytesResultTag = TryType24Tag; +pub type HostFileReadLineResult = TryType24; +pub type HostFileReadLineResultPayload = TryType24Payload; +pub type HostFileReadLineResultTag = TryType24Tag; +pub type HostFileReadUtf8Result = TryType27; +pub type HostFileReadUtf8ResultPayload = TryType27Payload; +pub type HostFileReadUtf8ResultTag = TryType27Tag; +pub type HostFileSizeInBytesResult = TryType31; +pub type HostFileSizeInBytesResultPayload = TryType31Payload; +pub type HostFileSizeInBytesResultTag = TryType31Tag; +pub type HostFileTimeAccessedResult = TryType33; +pub type HostFileTimeAccessedResultPayload = TryType33Payload; +pub type HostFileTimeAccessedResultTag = TryType33Tag; +pub type HostFileTimeCreatedResult = TryType33; +pub type HostFileTimeCreatedResultPayload = TryType33Payload; +pub type HostFileTimeCreatedResultTag = TryType33Tag; +pub type HostFileTimeModifiedResult = TryType33; +pub type HostFileTimeModifiedResultPayload = TryType33Payload; +pub type HostFileTimeModifiedResultTag = TryType33Tag; +pub type HostFileWriteBytesResult = TryType26; +pub type HostFileWriteBytesResultPayload = TryType26Payload; +pub type HostFileWriteBytesResultTag = TryType26Tag; +pub type HostFileWriteUtf8Result = TryType26; +pub type HostFileWriteUtf8ResultPayload = TryType26Payload; +pub type HostFileWriteUtf8ResultTag = TryType26Tag; +pub type HostHttpSendRequestResult = TryType35; +pub type HostHttpSendRequestResultPayload = TryType35Payload; +pub type HostHttpSendRequestResultTag = TryType35Tag; +pub type HostHttpSendRequestOk = AnonStruct37; +pub type HostLocaleGetResult = TryType42; +pub type HostLocaleGetResultPayload = TryType42Payload; +pub type HostLocaleGetResultTag = TryType42Tag; +pub type HostPathTypeResult = TryType44; +pub type HostPathTypeResultPayload = TryType44Payload; +pub type HostPathTypeResultTag = TryType44Tag; +pub type HostPathTypeOk = AnonStruct45; +pub type HostRandomSeedU32Result = TryType48; +pub type HostRandomSeedU32ResultPayload = TryType48Payload; +pub type HostRandomSeedU32ResultTag = TryType48Tag; +pub type HostRandomSeedU64Result = TryType46; +pub type HostRandomSeedU64ResultPayload = TryType46Payload; +pub type HostRandomSeedU64ResultTag = TryType46Tag; +pub type HostSqliteBindResult = TryType54; +pub type HostSqliteBindResultPayload = TryType54Payload; +pub type HostSqliteBindResultTag = TryType54Tag; +pub type HostSqliteBindErr = AnonStruct51; +pub type HostSqliteColumnValueResult = TryType59; +pub type HostSqliteColumnValueResultPayload = TryType59Payload; +pub type HostSqliteColumnValueResultTag = TryType59Tag; +pub type HostSqliteColumnValueErr = AnonStruct51; +pub type HostSqlitePrepareResult = TryType50; +pub type HostSqlitePrepareResultPayload = TryType50Payload; +pub type HostSqlitePrepareResultTag = TryType50Tag; +pub type HostSqlitePrepareErr = AnonStruct51; +pub type HostSqliteResetResult = TryType54; +pub type HostSqliteResetResultPayload = TryType54Payload; +pub type HostSqliteResetResultTag = TryType54Tag; +pub type HostSqliteResetErr = AnonStruct51; +pub type HostSqliteStepResult = TryType60; +pub type HostSqliteStepResultPayload = TryType60Payload; +pub type HostSqliteStepResultTag = TryType60Tag; +pub type HostSqliteStepErr = AnonStruct51; +pub type HostStderrLineResult = TryType61; +pub type HostStderrLineResultPayload = TryType61Payload; +pub type HostStderrLineResultTag = TryType61Tag; +pub type HostStderrWriteResult = TryType61; +pub type HostStderrWriteResultPayload = TryType61Payload; +pub type HostStderrWriteResultTag = TryType61Tag; +pub type HostStderrWriteBytesResult = TryType61; +pub type HostStderrWriteBytesResultPayload = TryType61Payload; +pub type HostStderrWriteBytesResultTag = TryType61Tag; +pub type HostStdinBytesResult = TryType65; +pub type HostStdinBytesResultPayload = TryType65Payload; +pub type HostStdinBytesResultTag = TryType65Tag; +pub type HostStdinLineResult = TryType63; +pub type HostStdinLineResultPayload = TryType63Payload; +pub type HostStdinLineResultTag = TryType63Tag; +pub type HostStdinReadToEndResult = TryType66; +pub type HostStdinReadToEndResultPayload = TryType66Payload; +pub type HostStdinReadToEndResultTag = TryType66Tag; +pub type HostStdoutLineResult = TryType68; +pub type HostStdoutLineResultPayload = TryType68Payload; +pub type HostStdoutLineResultTag = TryType68Tag; +pub type HostStdoutWriteResult = TryType68; +pub type HostStdoutWriteResultPayload = TryType68Payload; +pub type HostStdoutWriteResultTag = TryType68Tag; +pub type HostStdoutWriteBytesResult = TryType68; +pub type HostStdoutWriteBytesResultPayload = TryType68Payload; +pub type HostStdoutWriteBytesResultTag = TryType68Tag; +pub type HostTcpConnectResult = TryType70; +pub type HostTcpConnectResultPayload = TryType70Payload; +pub type HostTcpConnectResultTag = TryType70Tag; +pub type HostTcpReadExactlyResult = TryType72; +pub type HostTcpReadExactlyResultPayload = TryType72Payload; +pub type HostTcpReadExactlyResultTag = TryType72Tag; +pub type HostTcpReadUntilResult = TryType72; +pub type HostTcpReadUntilResultPayload = TryType72Payload; +pub type HostTcpReadUntilResultTag = TryType72Tag; +pub type HostTcpReadUpToResult = TryType72; +pub type HostTcpReadUpToResultPayload = TryType72Payload; +pub type HostTcpReadUpToResultTag = TryType72Tag; +pub type HostTcpWriteResult = TryType73; +pub type HostTcpWriteResultPayload = TryType73Payload; +pub type HostTcpWriteResultTag = TryType73Tag; + +// ============================================================================= +// 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(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(payload, amount); + }, + TryType0Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in IOErr. +pub fn decref_ioerr(value: IOErr, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + IOErrTag::AlreadyExists => {}, + IOErrTag::BrokenPipe => {}, + IOErrTag::Interrupted => {}, + IOErrTag::NotFound => {}, + IOErrTag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + IOErrTag::OutOfMemory => {}, + IOErrTag::PermissionDenied => {}, + IOErrTag::Unsupported => {}, + } +} + +/// Increment Roc-owned payloads in IOErr. +pub fn incref_ioerr(value: IOErr, amount: isize) { + let _ = amount; + match value.tag { + IOErrTag::AlreadyExists => {}, + IOErrTag::BrokenPipe => {}, + IOErrTag::Interrupted => {}, + IOErrTag::NotFound => {}, + IOErrTag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + IOErrTag::OutOfMemory => {}, + IOErrTag::PermissionDenied => {}, + IOErrTag::Unsupported => {}, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct4. +pub fn decref_anon_struct4(value: AnonStruct4, 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 AnonStruct4. +pub fn incref_anon_struct4(value: AnonStruct4, 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(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(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(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(payload, amount); + }, + TryType13Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType16. +pub fn decref_try_type16(value: TryType16, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType16Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType16Tag::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 TryType16. +pub fn incref_try_type16(value: TryType16, amount: isize) { + let _ = amount; + match value.tag { + TryType16Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType16Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// 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); + payload.decref(roc_host); + }, + TryType18Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.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); + payload.incref(amount); + }, + TryType18Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType20. +pub fn decref_try_type20(value: TryType20, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType20Tag::Err => {}, + TryType20Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType20. +pub fn incref_try_type20(value: TryType20, amount: isize) { + let _ = amount; + match value.tag { + TryType20Tag::Err => {}, + TryType20Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType22. +pub fn decref_try_type22(value: TryType22, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType22Tag::Err => {}, + TryType22Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType22. +pub fn incref_try_type22(value: TryType22, amount: isize) { + let _ = amount; + match value.tag { + TryType22Tag::Err => {}, + TryType22Tag::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 => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + 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 => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType24Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType26. +pub fn decref_try_type26(value: TryType26, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType26Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType26Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType26. +pub fn incref_try_type26(value: TryType26, amount: isize) { + let _ = amount; + match value.tag { + TryType26Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType26Tag::Ok => {}, + } +} + +/// 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 => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + 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 => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType27Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType28. +pub fn decref_try_type28(value: TryType28, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType28Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType28Tag::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 TryType28. +pub fn incref_try_type28(value: TryType28, amount: isize) { + let _ = amount; + match value.tag { + TryType28Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType28Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_box(payload as RocBox, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType31. +pub fn decref_try_type31(value: TryType31, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType31Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType31Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType31. +pub fn incref_try_type31(value: TryType31, amount: isize) { + let _ = amount; + match value.tag { + TryType31Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType31Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType32. +pub fn decref_try_type32(value: TryType32, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType32Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType32Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType32. +pub fn incref_try_type32(value: TryType32, amount: isize) { + let _ = amount; + match value.tag { + TryType32Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType32Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType33. +pub fn decref_try_type33(value: TryType33, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType33Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType33Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType33. +pub fn incref_try_type33(value: TryType33, amount: isize) { + let _ = amount; + match value.tag { + TryType33Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType33Tag::Ok => {}, + } +} + +/// 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_bad_body_or_network_error_or_other_or_timeout(payload, roc_host); + }, + TryType35Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + decref_anon_struct37(payload, roc_host); + }, + } +} + +/// 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_bad_body_or_network_error_or_other_or_timeout(payload, amount); + }, + TryType35Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_anon_struct37(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in BadBodyOrNetworkErrorOrOtherOrTimeout. +pub fn decref_bad_body_or_network_error_or_other_or_timeout(value: BadBodyOrNetworkErrorOrOtherOrTimeout, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::BadBody => {}, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::NetworkError => {}, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.decref(roc_host); + }, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::Timeout => {}, + } +} + +/// Increment Roc-owned payloads in BadBodyOrNetworkErrorOrOtherOrTimeout. +pub fn incref_bad_body_or_network_error_or_other_or_timeout(value: BadBodyOrNetworkErrorOrOtherOrTimeout, amount: isize) { + let _ = amount; + match value.tag { + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::BadBody => {}, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::NetworkError => {}, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::Other => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.other); + payload.incref(amount); + }, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::Timeout => {}, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct37. +pub fn decref_anon_struct37(value: AnonStruct37, 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_struct39(item, roc_host); + } + } + list.decref(roc_host); + } +} + +/// Increment Roc-owned fields in AnonStruct37. +pub fn incref_anon_struct37(value: AnonStruct37, amount: isize) { + value.body.incref(amount); + value.headers.incref(amount); +} + +/// Recursively decrement Roc-owned fields in AnonStruct39. +pub fn decref_anon_struct39(value: AnonStruct39, roc_host: &RocHost) { + value._0.decref(roc_host); + value._1.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct39. +pub fn incref_anon_struct39(value: AnonStruct39, amount: isize) { + value._0.incref(amount); + value._1.incref(amount); +} + +/// Recursively decrement Roc-owned fields in AnonStruct41. +pub fn decref_anon_struct41(value: AnonStruct41, 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_struct39(item, roc_host); + } + } + list.decref(roc_host); + } + value.method_ext.decref(roc_host); + value.uri.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct41. +pub fn incref_anon_struct41(value: AnonStruct41, 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 TryType42. +pub fn decref_try_type42(value: TryType42, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType42Tag::Err => {}, + TryType42Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType42. +pub fn incref_try_type42(value: TryType42, amount: isize) { + let _ = amount; + match value.tag { + TryType42Tag::Err => {}, + TryType42Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// 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(payload, roc_host); + }, + TryType44Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + decref_anon_struct45(payload, roc_host); + }, + } +} + +/// 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(payload, amount); + }, + TryType44Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_anon_struct45(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct45. +pub fn decref_anon_struct45(value: AnonStruct45, roc_host: &RocHost) { + let _ = value; + let _ = roc_host; +} + +/// Increment Roc-owned fields in AnonStruct45. +pub fn incref_anon_struct45(value: AnonStruct45, amount: isize) { + let _ = value; + let _ = amount; +} + +/// Recursively decrement Roc-owned payloads in TryType46. +pub fn decref_try_type46(value: TryType46, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType46Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType46Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType46. +pub fn incref_try_type46(value: TryType46, amount: isize) { + let _ = amount; + match value.tag { + TryType46Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType46Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType48. +pub fn decref_try_type48(value: TryType48, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType48Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType48Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType48. +pub fn incref_try_type48(value: TryType48, amount: isize) { + let _ = amount; + match value.tag { + TryType48Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType48Tag::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_anon_struct51(payload, roc_host); + }, + TryType50Tag::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 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_anon_struct51(payload, amount); + }, + TryType50Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_box(payload as RocBox, amount); + }, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct51. +pub fn decref_anon_struct51(value: AnonStruct51, roc_host: &RocHost) { + value.message.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct51. +pub fn incref_anon_struct51(value: AnonStruct51, amount: isize) { + value.message.incref(amount); +} + +/// Recursively decrement Roc-owned payloads in TryType54. +pub fn decref_try_type54(value: TryType54, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType54Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_anon_struct51(payload, roc_host); + }, + TryType54Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType54. +pub fn incref_try_type54(value: TryType54, amount: isize) { + let _ = amount; + match value.tag { + TryType54Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_anon_struct51(payload, amount); + }, + TryType54Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned fields in AnonStruct56. +pub fn decref_anon_struct56(value: AnonStruct56, roc_host: &RocHost) { + decref_bytes_or_integer_or_null_or_real_or_string(value.value, roc_host); + value.name.decref(roc_host); +} + +/// Increment Roc-owned fields in AnonStruct56. +pub fn incref_anon_struct56(value: AnonStruct56, amount: isize) { + incref_bytes_or_integer_or_null_or_real_or_string(value.value, amount); + value.name.incref(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 TryType59. +pub fn decref_try_type59(value: TryType59, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType59Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_anon_struct51(payload, roc_host); + }, + TryType59Tag::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 TryType59. +pub fn incref_try_type59(value: TryType59, amount: isize) { + let _ = amount; + match value.tag { + TryType59Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_anon_struct51(payload, amount); + }, + TryType59Tag::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 TryType60. +pub fn decref_try_type60(value: TryType60, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType60Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_anon_struct51(payload, roc_host); + }, + TryType60Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType60. +pub fn incref_try_type60(value: TryType60, amount: isize) { + let _ = amount; + match value.tag { + TryType60Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_anon_struct51(payload, amount); + }, + TryType60Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType61. +pub fn decref_try_type61(value: TryType61, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType61Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType61Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType61. +pub fn incref_try_type61(value: TryType61, amount: isize) { + let _ = amount; + match value.tag { + TryType61Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType61Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType63. +pub fn decref_try_type63(value: TryType63, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType63Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_end_of_file_or_stdin_err(payload, roc_host); + }, + TryType63Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType63. +pub fn incref_try_type63(value: TryType63, amount: isize) { + let _ = amount; + match value.tag { + TryType63Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_end_of_file_or_stdin_err(payload, amount); + }, + TryType63Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in EndOfFileOrStdinErr. +pub fn decref_end_of_file_or_stdin_err(value: EndOfFileOrStdinErr, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + EndOfFileOrStdinErrTag::EndOfFile => {}, + EndOfFileOrStdinErrTag::StdinErr => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.stdin_err); + decref_ioerr(payload, roc_host); + }, + } +} + +/// Increment Roc-owned payloads in EndOfFileOrStdinErr. +pub fn incref_end_of_file_or_stdin_err(value: EndOfFileOrStdinErr, amount: isize) { + let _ = amount; + match value.tag { + EndOfFileOrStdinErrTag::EndOfFile => {}, + EndOfFileOrStdinErrTag::StdinErr => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.stdin_err); + incref_ioerr(payload, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType65. +pub fn decref_try_type65(value: TryType65, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType65Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_end_of_file_or_stdin_err(payload, roc_host); + }, + TryType65Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType65. +pub fn incref_try_type65(value: TryType65, amount: isize) { + let _ = amount; + match value.tag { + TryType65Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_end_of_file_or_stdin_err(payload, amount); + }, + TryType65Tag::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(payload, roc_host); + }, + TryType66Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(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(payload, amount); + }, + TryType66Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType68. +pub fn decref_try_type68(value: TryType68, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType68Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + decref_ioerr(payload, roc_host); + }, + TryType68Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType68. +pub fn incref_try_type68(value: TryType68, amount: isize) { + let _ = amount; + match value.tag { + TryType68Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + incref_ioerr(payload, amount); + }, + TryType68Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType70. +pub fn decref_try_type70(value: TryType70, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType70Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.decref(roc_host); + }, + TryType70Tag::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 TryType70. +pub fn incref_try_type70(value: TryType70, amount: isize) { + let _ = amount; + match value.tag { + TryType70Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.incref(amount); + }, + TryType70Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + incref_box(payload as RocBox, amount); + }, + } +} + +/// Recursively decrement Roc-owned payloads in TryType72. +pub fn decref_try_type72(value: TryType72, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType72Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.decref(roc_host); + }, + TryType72Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.decref(roc_host); + }, + } +} + +/// Increment Roc-owned payloads in TryType72. +pub fn incref_try_type72(value: TryType72, amount: isize) { + let _ = amount; + match value.tag { + TryType72Tag::Err => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.err); + payload.incref(amount); + }, + TryType72Tag::Ok => unsafe { + let payload = core::mem::ManuallyDrop::into_inner(value.payload.ok); + payload.incref(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); + payload.decref(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); + payload.incref(amount); + }, + TryType73Tag::Ok => {}, + } +} + +/// Recursively decrement Roc-owned payloads in TryType77. +pub fn decref_try_type77(value: TryType77, roc_host: &RocHost) { + let _ = roc_host; + match value.tag { + TryType77Tag::Err => {}, + TryType77Tag::Ok => {}, + } +} + +/// Increment Roc-owned payloads in TryType77. +pub fn incref_try_type77(value: TryType77, amount: isize) { + let _ = amount; + match value.tag { + TryType77Tag::Err => {}, + TryType77Tag::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 Host.cmd_exec_exit_code! + /// Roc signature: { args : List(Str), clear_envs : Bool, envs : List(Str), program : Str } => Try(I32, IOErr) + pub fn hosted_cmd_host_exec_exit_code(arg0: HostCmdExecExitCodeArgs) -> TryType0; + + /// Hosted symbol for Host.cmd_exec_output! + /// Roc signature: { args : List(Str), clear_envs : Bool, envs : List(Str), program : Str } => 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: HostCmdExecOutputArgs) -> TryType7; + + /// Hosted symbol for Host.dir_create! + /// Roc signature: Str => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_create(arg0: RocStr) -> TryType13; + + /// Hosted symbol for Host.dir_create_all! + /// Roc signature: Str => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_create_all(arg0: RocStr) -> TryType13; + + /// Hosted symbol for Host.dir_delete_all! + /// Roc signature: Str => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_delete_all(arg0: RocStr) -> TryType13; + + /// Hosted symbol for Host.dir_delete_empty! + /// Roc signature: Str => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_delete_empty(arg0: RocStr) -> TryType13; + + /// Hosted symbol for Host.dir_list! + /// Roc signature: Str => Try(List(List(U8)), [DirErr(IOErr)]) + pub fn hosted_dir_list(arg0: RocStr) -> TryType16; + + /// Hosted symbol for Host.env_cwd! + /// Roc signature: {} => Try(List(U8), [CwdUnavailable]) + pub fn hosted_env_cwd() -> TryType20; + + /// Hosted symbol for Host.env_exe_path! + /// Roc signature: {} => Try(List(U8), [ExePathUnavailable]) + pub fn hosted_env_exe_path() -> TryType22; + + /// Hosted symbol for Host.env_temp_dir! + /// Roc signature: {} => List(U8) + pub fn hosted_env_temp_dir() -> RocListWith; + + /// Hosted symbol for Host.env_var! + /// Roc signature: Str => Try(Str, [VarNotFound(Str)]) + pub fn hosted_env_var(arg0: RocStr) -> TryType18; + + /// Hosted symbol for Host.file_delete! + /// Roc signature: Str => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_delete(arg0: RocStr) -> TryType26; + + /// Hosted symbol for Host.file_is_executable! + /// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_executable(arg0: RocStr) -> TryType32; + + /// Hosted symbol for Host.file_is_readable! + /// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_readable(arg0: RocStr) -> TryType32; + + /// Hosted symbol for Host.file_is_writable! + /// Roc signature: Str => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_writable(arg0: RocStr) -> TryType32; + + /// Hosted symbol for Host.file_open_reader! + /// Roc signature: Str, U64 => Try(Host.FileReader, [FileErr(IOErr)]) + pub fn hosted_file_open_reader(arg0: RocStr, arg1: u64) -> TryType28; + + /// Hosted symbol for Host.file_read_bytes! + /// Roc signature: Str => Try(List(U8), [FileErr(IOErr)]) + pub fn hosted_file_read_bytes(arg0: RocStr) -> TryType24; + + /// Hosted symbol for Host.file_read_line! + /// Roc signature: Host.FileReader => Try(List(U8), [FileErr(IOErr)]) + pub fn hosted_file_read_line(arg0: *mut u64) -> TryType24; + + /// Hosted symbol for Host.file_read_utf8! + /// Roc signature: Str => Try(Str, [FileErr(IOErr)]) + pub fn hosted_file_read_utf8(arg0: RocStr) -> TryType27; + + /// Hosted symbol for Host.file_size_in_bytes! + /// Roc signature: Str => Try(U64, [FileErr(IOErr)]) + pub fn hosted_file_size_in_bytes(arg0: RocStr) -> TryType31; + + /// Hosted symbol for Host.file_time_accessed! + /// Roc signature: Str => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_accessed(arg0: RocStr) -> TryType33; + + /// Hosted symbol for Host.file_time_created! + /// Roc signature: Str => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_created(arg0: RocStr) -> TryType33; + + /// Hosted symbol for Host.file_time_modified! + /// Roc signature: Str => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_modified(arg0: RocStr) -> TryType33; + + /// Hosted symbol for Host.file_write_bytes! + /// Roc signature: Str, List(U8) => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_write_bytes(arg0: RocStr, arg1: RocListWith) -> TryType26; + + /// Hosted symbol for Host.file_write_utf8! + /// Roc signature: Str, Str => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_write_utf8(arg0: RocStr, arg1: RocStr) -> TryType26; + + /// Hosted symbol for Host.http_send_request! + /// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str } => Try({ body : List(U8), headers : List((Str, Str)), status : U16 }, [BadBody, NetworkError, Other(List(U8)), Timeout]) + pub fn hosted_http_send_request(arg0: HostHttpSendRequestArgs) -> TryType35; + + /// Hosted symbol for Host.locale_all! + /// Roc signature: {} => List(Str) + pub fn hosted_locale_all() -> RocList; + + /// Hosted symbol for Host.locale_get! + /// Roc signature: {} => Try(Str, [NotAvailable]) + pub fn hosted_locale_get() -> TryType42; + + /// Hosted symbol for 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) -> TryType44; + + /// Hosted symbol for Host.random_seed_u32! + /// Roc signature: {} => Try(U32, [RandomErr(IOErr)]) + pub fn hosted_random_seed_u32() -> TryType48; + + /// Hosted symbol for Host.random_seed_u64! + /// Roc signature: {} => Try(U64, [RandomErr(IOErr)]) + pub fn hosted_random_seed_u64() -> TryType46; + + /// Hosted symbol for Host.sleep_millis! + /// Roc signature: U64 => {} + pub fn hosted_sleep_millis(arg0: u64); + + /// Hosted symbol for Host.sqlite_bind! + /// Roc signature: Host.SqliteStmt, 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) -> TryType54; + + /// Hosted symbol for Host.sqlite_column_value! + /// Roc signature: Host.SqliteStmt, 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) -> TryType59; + + /// Hosted symbol for Host.sqlite_columns! + /// Roc signature: Host.SqliteStmt => List(Str) + pub fn hosted_sqlite_columns(arg0: *mut u64) -> RocList; + + /// Hosted symbol for Host.sqlite_prepare! + /// Roc signature: Str, Str => Try(Host.SqliteStmt, { code : I64, message : Str }) + pub fn hosted_sqlite_prepare(arg0: RocStr, arg1: RocStr) -> TryType50; + + /// Hosted symbol for Host.sqlite_reset! + /// Roc signature: Host.SqliteStmt => Try({}, { code : I64, message : Str }) + pub fn hosted_sqlite_reset(arg0: *mut u64) -> TryType54; + + /// Hosted symbol for Host.sqlite_step! + /// Roc signature: Host.SqliteStmt => Try(Bool, { code : I64, message : Str }) + pub fn hosted_sqlite_step(arg0: *mut u64) -> TryType60; + + /// Hosted symbol for Host.stderr_line! + /// Roc signature: Str => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_line(arg0: RocStr) -> TryType61; + + /// Hosted symbol for Host.stderr_write! + /// Roc signature: Str => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_write(arg0: RocStr) -> TryType61; + + /// Hosted symbol for Host.stderr_write_bytes! + /// Roc signature: List(U8) => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_write_bytes(arg0: RocListWith) -> TryType61; + + /// Hosted symbol for Host.stdin_bytes! + /// Roc signature: {} => Try(List(U8), [EndOfFile, StdinErr(IOErr)]) + pub fn hosted_stdin_bytes() -> TryType65; + + /// Hosted symbol for Host.stdin_line! + /// Roc signature: {} => Try(Str, [EndOfFile, StdinErr(IOErr)]) + pub fn hosted_stdin_line() -> TryType63; + + /// Hosted symbol for Host.stdin_read_to_end! + /// Roc signature: {} => Try(List(U8), [StdinErr(IOErr)]) + pub fn hosted_stdin_read_to_end() -> TryType66; + + /// Hosted symbol for Host.stdout_line! + /// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_line(arg0: RocStr) -> TryType68; + + /// Hosted symbol for Host.stdout_write! + /// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_write(arg0: RocStr) -> TryType68; + + /// Hosted symbol for Host.stdout_write_bytes! + /// Roc signature: List(U8) => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_write_bytes(arg0: RocListWith) -> TryType68; + + /// Hosted symbol for Host.tcp_connect! + /// Roc signature: Str, U16 => Try(Host.TcpStream, Str) + pub fn hosted_tcp_connect(arg0: RocStr, arg1: u16) -> TryType70; + + /// Hosted symbol for Host.tcp_read_exactly! + /// Roc signature: Host.TcpStream, U64 => Try(List(U8), Str) + pub fn hosted_tcp_read_exactly(arg0: *mut u64, arg1: u64) -> TryType72; + + /// Hosted symbol for Host.tcp_read_until! + /// Roc signature: Host.TcpStream, U8 => Try(List(U8), Str) + pub fn hosted_tcp_read_until(arg0: *mut u64, arg1: u8) -> TryType72; + + /// Hosted symbol for Host.tcp_read_up_to! + /// Roc signature: Host.TcpStream, U64 => Try(List(U8), Str) + pub fn hosted_tcp_read_up_to(arg0: *mut u64, arg1: u64) -> TryType72; + + /// Hosted symbol for Host.tcp_write! + /// Roc signature: Host.TcpStream, List(U8) => Try({}, Str) + pub fn hosted_tcp_write(arg0: *mut u64, arg1: RocListWith) -> TryType73; + + /// Hosted symbol for Host.tty_disable_raw_mode! + /// Roc signature: {} => {} + pub fn hosted_tty_disable_raw_mode(); + + /// Hosted symbol for Host.tty_enable_raw_mode! + /// Roc signature: {} => {} + pub fn hosted_tty_enable_raw_mode(); + + /// Hosted symbol for Host.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..0f47e974 100644 --- a/tests/cmd-test.roc +++ b/tests/cmd-test.roc @@ -2,118 +2,114 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout import pf.Cmd -import pf.Arg exposing [Arg] +import pf.IOErr exposing [IOErr] # Tests all error cases in Cmd functions. -main! : List Arg => Result {} _ -main! = |_args| +main! : List(Str) => Try( + {}, + [ + ExecFailed({ command : Str, exit_code : I32 }), + Exit(I32), + FailedExpectation(Str), + FailedToGetExitCode({ command : Str, err : IOErr }), + StdoutErr(IOErr), + ], +) +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 - Ok({})? - else - Err(FailedExpectation( - """ - - - Expected: - 1 + .arg("non_existent.txt") + .exec_exit_code!()? - - Got: - ${Inspect.to_str(exit_code)} - - """ - ))? + if exit_code == 1 { + Ok({})? + } else { + message : Str + message = + \\- Expected: + \\1 + \\ + \\- Got: + \\${Str.inspect(exit_code)} + Err(FailedExpectation(message))? + } Stdout.line!("All tests passed.")? Ok({}) +} -expect_err = |err, expected_str| - if Inspect.to_str(err) == expected_str then +expect_err : _, Str -> Try({}, [FailedExpectation(Str)]) +expect_err = |err, expected_str| { + if Str.inspect(err) == expected_str { Ok({}) - else - Err(FailedExpectation( - """ - - - Expected: - ${expected_str} - - - Got: - ${Inspect.to_str(err)} - - """ - )) \ No newline at end of file + } else { + message : Str + message = + \\- Expected: + \\${expected_str} + + \\- Got: + \\${Str.inspect(err)} + Err(FailedExpectation(message)) + } +} 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