diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3ea1498..4c9348ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,78 +1,124 @@ +name: CI + 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-native: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-arm, macos-15] + validate: + name: validate sources + runs-on: ubuntu-22.04 + permissions: + contents: read steps: - uses: actions/checkout@v4 - - - 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 - - - uses: roc-lang/setup-roc@39c354a6a838a0089eea9068a0414f49b62c5c08 + - 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 - - - run: roc version - - - name: Install dependencies (Ubuntu) - if: startsWith(matrix.os, 'ubuntu-') - run: | - sudo apt install -y expect ncat ripgrep - - - name: Install dependencies (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 - - - name: Run all tests - run: ROC=roc EXAMPLES_DIR=./examples/ ./ci/all_tests.sh + version: 0.16.0 + - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 + with: + version: nightly-new-compiler + - uses: dtolnay/rust-toolchain@1.83.0 + with: + targets: x86_64-unknown-linux-musl + - name: Format, check, and test examples + run: ./scripts/test.py --operation validate - - 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 + validate-windows: + name: validate sources (windows) + runs-on: windows-2025 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 + with: + version: nightly-new-compiler + - uses: dtolnay/rust-toolchain@1.83.0 + with: + targets: x86_64-pc-windows-msvc + - name: Format, check, and test examples + shell: bash + run: ./scripts/test.py --operation validate - - 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 + build-examples: + name: build examples (${{ matrix.target }}) + needs: validate + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - target: x64mac + os: macos-15 + rust_target: x86_64-apple-darwin + - target: arm64mac + os: macos-15 + rust_target: aarch64-apple-darwin + - target: x64musl + os: ubuntu-22.04 + rust_target: x86_64-unknown-linux-musl + - target: arm64musl + os: ubuntu-22.04 + rust_target: aarch64-unknown-linux-musl + - target: x64win + os: windows-2025 + rust_target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Install Zig + if: runner.os != 'Windows' + uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # ratchet:mlugg/setup-zig@v2.0.5 + with: + version: 0.16.0 + - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 + with: + version: nightly-new-compiler + - uses: dtolnay/rust-toolchain@1.83.0 + with: + targets: ${{ matrix.rust_target }} + - name: Cross-compile every example + shell: bash + run: ./scripts/test.py --operation build --target "${{ matrix.target }}" --artifact-dir dist/example-binaries + - name: Upload example binaries + uses: actions/upload-artifact@v4 + with: + name: basic-cli-example-binaries-${{ matrix.target }} + path: dist/example-binaries/${{ matrix.target }} + if-no-files-found: error - - 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 + run-examples: + name: run examples (${{ matrix.target }}) + needs: build-examples + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - target: x64mac + os: macos-15-intel + - target: arm64mac + os: macos-15 + - target: x64musl + os: ubuntu-22.04 + - target: x64win + os: windows-2025 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.83.0 + - name: Download native example binaries + uses: actions/download-artifact@v4 + with: + name: basic-cli-example-binaries-${{ matrix.target }} + path: dist/example-binaries/${{ matrix.target }} + - name: Run native spec cases + shell: bash + run: ./scripts/test.py --operation run --target "${{ matrix.target }}" --artifact-dir dist/example-binaries 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..df07ae01 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -5,109 +5,70 @@ on: branches: - main paths: - - '**.roc' - release: - types: - - created - + - ".github/workflows/deploy-docs.yml" + - "docs/**" + - "platform/**" + - "www/**" 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 + runs-on: ubuntu-latest 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 + - name: Install Roc + uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b with: - # Note: nightly hashes are not verified because they are updated regularly. - version: nightly + version: nightly-new-compiler - - run: roc version - - - name: Create temp directory for docs - run: mkdir -p ./temp_docs - - - name: Download and extract docs for each release + - name: Create Pages tree run: | - # Get all releases - releases=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/roc-lang/basic-cli/releases" | jq -c '.') - echo "$releases" | jq -c '.[]' | while read -r release; do - release_name=$(echo $release | jq -r '.tag_name') - assets_url=$(echo $release | jq -r '.assets_url') - - # Get assets for this release - assets=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "${assets_url}") - - # Look for docs.tar.gz asset - download_url=$(echo $assets | jq -r '.[] | select(.name=="docs.tar.gz") | .browser_download_url') - - if [ ! -z "$download_url" ]; then - echo "Processing release ${release_name}, downloading from ${download_url}" - - # Create directory for this release - mkdir -p "./temp_docs/${release_name}" - - # Download and extract - curl -sL "${download_url}" -o ./temp_docs/temp.tar.gz - tar -xzf ./temp_docs/temp.tar.gz -C "./temp_docs/${release_name}" --strip-components=1 - rm ./temp_docs/temp.tar.gz - else - echo "Error: docs.tar.gz not found for release ${release_name}" - fi - done - - # fix URLs - find ./temp_docs -type f -exec sed -i 's/\/packages\/basic-cli\//\/basic-cli\//g' {} + - - # Get the latest release version - latest_release=$(echo "${releases}" | jq -r '.[0].tag_name') - - if [ -f "./docs/index.html" ]; then - # Copy the index.html and replace LATESTVERSION with actual latest release - cat ./docs/index.html | sed "s/LATESTVERSION/${latest_release}/g" > ./temp_docs/index.html - echo "Created index.html with latest version: ${latest_release}" + mkdir -p temp_docs + if [ -d www ]; then + cp -R www/. temp_docs/ else - echo "Error: index.html not found in docs folder" - exit 1 + sed 's/LATESTVERSION/main/g' docs/index.html > temp_docs/index.html 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 - mkdir -p "./temp_docs/main" + roc docs --output=temp_docs/main platform/main.roc - mv ./generated-docs/* ./temp_docs/main - + - name: Fix legacy generated URLs + run: | + 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" - - - name: Deploy to GitHub Pages + path: temp_docs + + - name: Deploy 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..e77935ed --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,467 @@ +name: Release + +on: + workflow_dispatch: + inputs: + release_version: + description: "Release version, for example 0.21.0 or 0.21.0-rc1" + required: true + type: string + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.event_name == 'workflow_dispatch' && format('release-{0}', github.repository) || format('release-validate-{0}-{1}', github.repository, github.ref) }} + cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} + +jobs: + build-windows-host: + name: Build Windows x64 host + runs-on: windows-2025 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@1.83.0 + with: + targets: x86_64-pc-windows-msvc + + - name: Build Windows host + shell: pwsh + run: python scripts/build.py + + - name: Upload Windows host + uses: actions/upload-artifact@v4 + with: + name: windows-x64-host + path: platform/targets/x64win/*.lib + if-no-files-found: error + retention-days: 7 + + build: + name: Build release bundle + needs: build-windows-host + # macOS cross-compiles the Apple and Linux musl hosts. Windows is built + # natively above because the Rust dependencies require the Windows SDK. + runs-on: macos-15 + outputs: + release_version: ${{ steps.validate.outputs.release_version }} + docs_version: ${{ steps.validate.outputs.docs_version }} + test_matrix: ${{ steps.bundles.outputs.test_matrix }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Roc + uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b + with: + version: nightly-new-compiler + + - name: Install Zig + uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f + with: + version: 0.16.0 + + - name: Install Rust with cross-compilation targets + uses: dtolnay/rust-toolchain@1.83.0 + with: + targets: x86_64-apple-darwin,aarch64-apple-darwin,x86_64-unknown-linux-musl,aarch64-unknown-linux-musl + + - name: Download Windows host + uses: actions/download-artifact@v4 + with: + name: windows-x64-host + path: platform/targets/x64win + + - name: Validate release request + id: validate + uses: roc-lang/release-package/actions/validate-release@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + release_version: ${{ github.event_name == 'workflow_dispatch' && inputs.release_version || '' }} + dry_run: ${{ github.event_name != 'workflow_dispatch' }} + github_token: ${{ github.token }} + + - name: Validate release branch + if: github.event_name == 'workflow_dispatch' + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + if [ "${GITHUB_REF_TYPE:-}" != "branch" ] || [ "$GITHUB_REF_NAME" != "$DEFAULT_BRANCH" ]; then + echo "Releases must be dispatched from the default branch: $DEFAULT_BRANCH" >&2 + exit 1 + fi + + - name: Build platform for all targets + run: ./scripts/build.py --all + + - name: Validate package + env: + NO_BUILD: "1" + run: ./scripts/test.py + + - name: Resolve previous release + if: github.event_name == 'workflow_dispatch' + id: previous + uses: roc-lang/release-package/actions/resolve-previous-release@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + github_token: ${{ github.token }} + + # Bootstrap exception: 0.20.0 is a .tar.br bundle in old Roc syntax and + # alpha-0 is also no longer parseable, so current `roc bump` cannot compare + # either predecessor. Change this to `require` after the first stable + # new-compiler .tar.zst release. + - name: Check version bump + uses: roc-lang/release-package/actions/run-bump-check@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + release_version: ${{ steps.validate.outputs.release_version }} + dry_run: ${{ github.event_name != 'workflow_dispatch' }} + bump_check: warn + bump_entrypoint: platform/main.roc + previous_url: ${{ steps.previous.outputs.previous_url }} + + - name: Bundle platform + run: ./scripts/bundle.py --output-dir dist + + - name: Prepare release bundle metadata + id: bundles + uses: roc-lang/release-package/actions/prepare-bundles@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + bundle_glob: dist/*.tar.zst + test_os_json: '["macos-15","macos-15-intel","ubuntu-22.04","windows-2025"]' + + - name: Upload release bundles + uses: actions/upload-artifact@v4 + with: + name: release-bundles + path: .release/bundles/* + if-no-files-found: error + retention-days: 7 + + - name: Upload release metadata + uses: actions/upload-artifact@v4 + with: + name: release-metadata + path: | + .release/bump-output.txt + .release/previous-url.txt + .release/release-bundles.json + .release/test-matrix.json + if-no-files-found: error + retention-days: 7 + + test-bundles: + name: Test ${{ matrix.bundle_name }} bundle (${{ matrix.os }}) + needs: build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.build.outputs.test_matrix) }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Roc + uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b + with: + version: nightly-new-compiler + + - name: Install Rust + uses: dtolnay/rust-toolchain@1.83.0 + + - name: Download release bundles + uses: actions/download-artifact@v4 + with: + name: release-bundles + path: .release/test-bundles + + - name: Test release bundle (Unix) + if: matrix.os != 'windows-2025' + uses: roc-lang/release-package/actions/test-bundle@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + test_bundle_command: ./scripts/test.py --bundle-path + bundle_path: .release/test-bundles/${{ matrix.artifact_file }} + bundle_name: ${{ matrix.bundle_name }} + release_version: ${{ needs.build.outputs.release_version }} + + - name: Test release bundle (Windows) + if: matrix.os == 'windows-2025' + uses: roc-lang/release-package/actions/test-bundle@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + test_bundle_command: python scripts/test.py --bundle-path + bundle_path: .release/test-bundles/${{ matrix.artifact_file }} + bundle_name: ${{ matrix.bundle_name }} + release_version: ${{ needs.build.outputs.release_version }} + + build-docs: + name: Build docs archive + needs: build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Roc + uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b + with: + version: nightly-new-compiler + + - name: Create docs staging tree + run: mkdir -p docs-site + + - name: Snapshot docs staging tree + uses: roc-lang/release-package/actions/docs-snapshot@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + docs_root: docs-site + + - name: Generate docs archive + env: + DOCS_URL_ROOT: /basic-cli/${{ needs.build.outputs.docs_version }} + DOCS_VERSION: ${{ needs.build.outputs.docs_version }} + run: | + set -euo pipefail + + ROC_DOCS_URL_ROOT="$DOCS_URL_ROOT" roc docs --output="docs-site/$DOCS_VERSION" platform/main.roc + + - name: Validate versioned docs + uses: roc-lang/release-package/actions/docs-validate@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + docs_root: docs-site + docs_version: ${{ needs.build.outputs.docs_version }} + + - name: Archive versioned docs + env: + DOCS_VERSION: ${{ needs.build.outputs.docs_version }} + run: tar -czf docs.tar.gz -C docs-site "$DOCS_VERSION" + + - name: Upload docs archive + uses: actions/upload-artifact@v4 + with: + name: platform-docs + path: docs.tar.gz + if-no-files-found: error + retention-days: 7 + + publish-release: + name: Publish GitHub release + needs: + - build + - build-docs + - test-bundles + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download release bundles + uses: actions/download-artifact@v4 + with: + name: release-bundles + path: .release/bundles + + - name: Download release metadata + uses: actions/download-artifact@v4 + with: + name: release-metadata + path: .release + + - name: Download docs archive + uses: actions/download-artifact@v4 + with: + name: platform-docs + + - name: Make release notes + uses: roc-lang/release-package/actions/make-release-notes@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + release_version: ${{ needs.build.outputs.release_version }} + docs_url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${{ needs.build.outputs.docs_version }}/ + github_token: ${{ github.token }} + + - name: Publish release + uses: roc-lang/release-package/actions/publish-release@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + release_version: ${{ needs.build.outputs.release_version }} + github_token: ${{ github.token }} + + - name: Attach docs archive + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ needs.build.outputs.release_version }} + run: gh release upload "$RELEASE_VERSION" docs.tar.gz --repo "$GITHUB_REPOSITORY" + + publish-docs: + name: Publish versioned docs and update examples + needs: + - build + - publish-release + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Roc + uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b + with: + version: nightly-new-compiler + + - name: Download release metadata + uses: actions/download-artifact@v4 + with: + name: release-metadata + path: .release + + - name: Derive release bundle URL and update examples + env: + RELEASE_VERSION: ${{ needs.build.outputs.release_version }} + run: | + bundle_url="$(python3 - <<'PY' + import json + import os + from pathlib import Path + + bundles = json.loads(Path(".release/release-bundles.json").read_text(encoding="utf-8")) + if len(bundles) != 1: + raise SystemExit(f"expected exactly one release bundle, found {len(bundles)}") + + artifact_file = bundles[0]["artifact_file"] + print( + f"https://github.com/{os.environ['GITHUB_REPOSITORY']}/releases/" + f"download/{os.environ['RELEASE_VERSION']}/{artifact_file}" + ) + PY + )" + ./scripts/update_app_platform_urls.py \ + --platform-url "$bundle_url" \ + examples + + - name: Validate updated examples + run: | + for example in examples/*.roc; do + roc check "$example" --no-cache + roc test "$example" --no-cache + done + + - name: Restore published versioned docs + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + mkdir -p www .release/docs-downloads + releases_json="$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100")" + latest_stable="$(printf '%s' "$releases_json" | jq -r '[.[] | select(.draft == false and .prerelease == false)][0].tag_name // empty')" + + printf '%s' "$releases_json" | jq -r '.[] | select(.draft == false) | .tag_name' | while IFS= read -r release_name; do + [ -n "$release_name" ] || continue + download_dir=".release/docs-downloads/$release_name" + mkdir -p "$download_dir" + + if gh release download "$release_name" --pattern docs.tar.gz --dir "$download_dir" --clobber; then + mkdir -p "www/$release_name" + tar -xzf "$download_dir/docs.tar.gz" -C "www/$release_name" --strip-components=1 + echo "Restored docs for $release_name" + else + echo "No docs.tar.gz asset for $release_name; skipping" + fi + done + + if [ -n "$latest_stable" ]; then + sed "s/LATESTVERSION/$latest_stable/g" docs/index.html > www/index.html + fi + + - name: Snapshot existing docs + uses: roc-lang/release-package/actions/docs-snapshot@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + docs_root: www + + - name: Generate release docs + env: + DOCS_URL_ROOT: /basic-cli/${{ needs.build.outputs.docs_version }} + DOCS_VERSION: ${{ needs.build.outputs.docs_version }} + run: | + rm -rf "www/$DOCS_VERSION" + ROC_DOCS_URL_ROOT="$DOCS_URL_ROOT" roc docs \ + --output="www/$DOCS_VERSION" \ + platform/main.roc + + - name: Update docs index + uses: roc-lang/release-package/actions/docs-index@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + docs_root: www + docs_version: ${{ needs.build.outputs.docs_version }} + + - name: Validate docs + uses: roc-lang/release-package/actions/docs-validate@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + docs_root: www + docs_version: ${{ needs.build.outputs.docs_version }} + + - name: Open release follow-up PR + uses: roc-lang/release-package/actions/create-followup-pr@d2560ead9f724bfaabfbc8134b8895e120171064 + with: + release_version: ${{ needs.build.outputs.release_version }} + paths: | + examples + www + branch_prefix: release-followup + base_branch: ${{ github.event.repository.default_branch }} + commit_message: Update docs and examples for ${{ needs.build.outputs.release_version }} + pr_title: Update docs and examples for ${{ needs.build.outputs.release_version }} + pr_body: | + Release follow-up for ${{ needs.build.outputs.release_version }}. + + Updates checked-in examples to the published platform bundle and + adds the generated versioned documentation. + github_token: ${{ github.token }} + + - name: Create Pages tree + run: | + mkdir -p temp_docs + cp -R www/. temp_docs/ + + # Keep /main live without committing a generated main snapshot. + ROC_DOCS_URL_ROOT=/basic-cli/main roc docs \ + --output=temp_docs/main \ + platform/main.roc + + - name: Fix legacy generated URLs + run: | + find temp_docs -type f \( -name "*.html" -o -name "*.js" \) \ + -exec sed -i 's|/packages/basic-cli/|/basic-cli/|g' {} + + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: temp_docs + + - name: Deploy Pages + id: deployment + uses: actions/deploy-pages@v4 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..51f08051 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,33 +58,43 @@ 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 dirExampleA dirExampleD -# see ci/all_tests.sh -all_tests_output.log +# Test runner output +test_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 +dist/ +.release/ + +# Python helper caches +__pycache__/ +*.pyc + +# Locally downloaded Roc nightly +.roc-bin/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23d78af0..ae556802 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,138 @@ # 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 download the latest archive +for your operating system from the +[`roc-lang/nightlies` releases](https://github.com/roc-lang/nightlies/releases/latest). + ## 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, extract the downloaded archive and add +the directory containing the `roc` executable to your `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 `./scripts/test.py`. + +## Verification + +Run the full local check before opening release or CI-facing changes: + +```sh +./scripts/test.py +``` + +The default command builds and bundles the native host, serves the bundle from +localhost, then formats, checks, tests, builds, and runs every example. Process +input, environment, fixtures, helper servers, exit codes, and separate stdout +and stderr assertions work on Unix and Windows. + +The data in `scripts/test_spec.json` is the source of truth for the test matrix. +Every example must have exactly one entry. Set its `enabled` flag to `false` +to skip the app, or set a stage flag to `false` under `stages` or +`platforms.windows` to skip only a broken stage without changing the runner. +Add named objects to an app's `cases` array to run the same compiled binary +with different arguments, stdin, environment, fixtures, helper servers, +expected exit codes, or output assertions. `happy` is only a naming convention; +an app can have any number of successful and failing cases. + +CI separates source validation, cross-target compilation, and native execution. +Every example is compiled for each target declared in `platform/main.roc`: +`x64mac`, `arm64mac`, `x64win`, `x64musl`, and `arm64musl`. Target-specific +binary artifacts are then downloaded and executed on matching native runners; +`arm64musl` remains compile-only until an arm64 Linux runner is available. + +The operations can also be run independently: + +```sh +./scripts/test.py --operation validate +./scripts/test.py --operation build --target x64musl --artifact-dir dist/example-binaries +./scripts/test.py --operation run --target x64musl --artifact-dir dist/example-binaries +``` + +For faster local iterations when the platform host is already built: + +```sh +./scripts/test.py --no-build +``` + +Build and validation operations bundle the current platform and temporarily +rewrite example headers to use its localhost URL. Checked-in examples may +therefore keep using the latest published release URL while local work and pull +requests exercise the WIP platform. + +## 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. + +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 are executable documentation for representative, realistic workflows; +they are not intended to exhaustively exercise every public API function. + +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 platform entrypoint: + +```sh +ROC_DOCS_URL_ROOT=/basic-cli/main roc docs --output=generated-docs platform/main.roc +``` -## How to generate docs? +The documentation entrypoint is `platform/main.roc`, matching the package that +applications consume. -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 +The release workflow attaches `docs.tar.gz`, updates checked-in examples to the +new bundle URL, generates versioned docs under `www/`, and opens a follow-up PR +for those source changes. It also deploys the validated docs immediately. After +the follow-up PR merges, the Pages workflow deploys the committed release docs +plus freshly generated `main` docs. 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..95925836 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,66 +1,38 @@ -[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" +rust-version = "1.83" 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" +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(no_roc_std_helpers)"] } + +[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..e6e97c3d 100644 --- a/README.md +++ b/README.md @@ -5,29 +5,30 @@ # 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 contains executable, application-shaped CLI +programs for common tasks like reading files, running commands, constructing +URLs, 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 and encoder directly through +`Http.get!` and `Http.send_json!`. + +## 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/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/ci/all_tests.sh b/ci/all_tests.sh deleted file mode 100755 index 2d739e1a..00000000 --- a/ci/all_tests.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env bash - -# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ -set -exo pipefail - -if [ -z "${EXAMPLES_DIR}" ]; then - echo "ERROR: The EXAMPLES_DIR environment variable is not set." >&2 - - exit 1 -else - EXAMPLES_DIR=$(realpath "${EXAMPLES_DIR}")/ -fi - -if [ -z "${ROC}" ]; then - echo "ERROR: The ROC environment variable is not set. - Set it to something like: - /home/username/Downloads/roc_nightly-linux_x86_64-2023-10-30-cb00cfb/roc - or - /home/username/gitrepos/roc/target/build/release/roc" >&2 - - exit 1 -fi - -TESTS_DIR="$(dirname "$EXAMPLES_DIR")/tests/" -export TESTS_DIR - -if [ "$NO_BUILD" != "1" ]; then - # May be needed for breaking roc changes. Also replace platform in build.roc with `cli: platform "platform/main.roc",` - ./jump-start.sh - - # build the basic-cli platform - $ROC dev ./build.roc --linker=legacy -- --roc $ROC -fi - -# roc check -for roc_file in $EXAMPLES_DIR*.roc; do - $ROC check $roc_file -done -for roc_file in $TESTS_DIR*.roc; do - $ROC check $roc_file -done - -$ROC ci/check_all_exposed_funs_tested.roc -$ROC ci/check_cargo_versions_match.roc - -# roc build -architecture=$(uname -m) - -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") - - if [ "$base_file" == "temp-dir.roc" ]; then - $ROC build $roc_file $ROC_BUILD_FLAGS --linker=legacy - else - $ROC build $roc_file $ROC_BUILD_FLAGS - fi - -done -for roc_file in $TESTS_DIR*.roc; do - $ROC build $roc_file $ROC_BUILD_FLAGS -done - -# Check for duplicate .roc file names between EXAMPLES_DIR and TESTS_DIR (this messes with checks) -for example_file in $EXAMPLES_DIR*.roc; do - example_basename=$(basename "$example_file") - if [ -f "$TESTS_DIR$example_basename" ]; then - echo "ERROR: Duplicate file name found: $example_basename exists in both $EXAMPLES_DIR and $TESTS_DIR. Change the name of one of them." >&2 - exit 1 - fi -done - -# prep for next step -cd ci/rust_http_server -cargo build --release -cd ../.. - -# check output with linux expect -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") - - # Skip env-var.roc when on aarch64 - if [ "$architecture" == "aarch64" ] && [ "$base_file" == "env-var.roc" ]; then - continue - fi - - ## 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 - 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 -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 -done - -# remove Dir example directorys if they exist -rm -rf dirExampleE -rm -rf dirExampleA -rm -rf dirExampleD - -# countdown, echo, form... all require user input or special setup -ignore_list=("stdin-basic.roc" "stdin-pipe.roc" "command-line-args.roc" "http.roc" "env-var.roc" "bytes-stdin-stdout.roc" "error-handling.roc" "tcp-client.roc" "tcp.roc" "terminal-app-snake.roc") - -# roc dev (some expects only run with `roc dev`) -for roc_file in $EXAMPLES_DIR*.roc; do - base_file=$(basename "$roc_file") - - - # check if base_file matches something from ignore_list - for file in "${ignore_list[@]}"; do - if [ "$base_file" == "$file" ]; then - continue 2 # continue the outer loop if a match is found - fi - done - - # For path.roc we need be inside the EXAMPLES_DIR - if [ "$base_file" == "path.roc" ]; then - absolute_roc=$(which $ROC | xargs realpath) - cd $EXAMPLES_DIR - $absolute_roc dev $base_file $ROC_BUILD_FLAGS - cd .. - elif [ "$base_file" == "sqlite-basic.roc" ]; then - DB_PATH=${EXAMPLES_DIR}todos.db $ROC dev $roc_file $ROC_BUILD_FLAGS - elif [ "$base_file" == "sqlite-everything.roc" ]; then - DB_PATH=${EXAMPLES_DIR}todos2.db $ROC dev $roc_file $ROC_BUILD_FLAGS - elif [ "$base_file" == "temp-dir.roc" ]; then - $ROC dev $roc_file $ROC_BUILD_FLAGS --linker=legacy - elif [ "$base_file" == "file-accessed-modified-created-time.roc" ] && [ "$IS_MUSL" == "1" ]; then - continue - else - - $ROC dev $roc_file $ROC_BUILD_FLAGS - 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 - else - $ROC dev $roc_file $ROC_BUILD_FLAGS - 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 - fi -done - -# test building website -$ROC docs platform/main.roc 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/bytes-stdin-stdout.exp b/ci/expect_scripts/bytes-stdin-stdout.exp deleted file mode 100644 index e6a652c8..00000000 --- a/ci/expect_scripts/bytes-stdin-stdout.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)bytes-stdin-stdout - -send -- "someinput\r" - -set expected_output [normalize_output { -someinput -someinput -}] - -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/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 deleted file mode 100755 index 074659d8..00000000 --- a/ci/expect_scripts/command-line-args.exp +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -spawn $env(EXAMPLES_DIR)/command-line-args foo - -source ./ci/expect_scripts/shared-code.exp - - -set expected_output [normalize_output { -received argument: foo -Unix argument, bytes: \[102, 111, 111\] -back to Arg: "foo" -}] - -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/command.exp b/ci/expect_scripts/command.exp deleted file mode 100644 index 59df35fb..00000000 --- a/ci/expect_scripts/command.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 - -spawn $env(EXAMPLES_DIR)command - - -set expected_output [normalize_output { -Hello -\{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\]\} -}] - -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/dir.exp b/ci/expect_scripts/dir.exp deleted file mode 100644 index e07abda0..00000000 --- a/ci/expect_scripts/dir.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)dir - -expect "Success!\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/env-var.exp b/ci/expect_scripts/env-var.exp deleted file mode 100644 index 63ae3710..00000000 --- a/ci/expect_scripts/env-var.exp +++ /dev/null @@ -1,28 +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(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 { - 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/env.exp b/ci/expect_scripts/env.exp deleted file mode 100644 index 975da4ea..00000000 --- a/ci/expect_scripts/env.exp +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(TESTS_DIR)env - -expect "Testing Env module functions..." { - expect "Testing Env.cwd!:" { - # Match cwd path that has ArbitraryBytes with non-empty list of integers - expect -re {cwd: /[^\r\n]*\r\n} { - - expect "Testing Env.exe_path!:" { - # Match exe_path with any valid format - expect -re {exe_path: /[^\r\n]*\r\n} { - - expect "Testing Env.platform!:" { - # Match platform info with any arch and OS - # Literal braces in regex need to be escaped: \{ and \} - expect -re {Current platform:\{arch: \w+, os: \w+\}} { - - expect "Testing Env.dict!:" { - # Match environment variables count as non-zero number - expect -re {Environment variables count: (\d+)} { - set env_count $expect_out(1,string) - if {$env_count < 1} { - puts stderr "\nExpect script failed: environment variable count is $env_count." - exit 1 - } - # Match sample environment variables with non-empty strings - # Literal brackets [], parentheses (), and quotes "" need escaping or careful handling - expect -re {Sample environment variables:\[\("\w+", ".*"\)(, \("\w+", ".*"\))*\]} { - - expect "Testing Env.set_cwd!:" { - # Match changed directory path with non-empty list of integers - expect -re {Changed current directory to: /[^\r\n]*\r\n} { - - expect "All tests executed." { - expect eof { - check_exit_and_segfault - } - } - } - } - } - } - } - } - } - } - } - } - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/error-handling.exp b/ci/expect_scripts/error-handling.exp deleted file mode 100644 index 25b9bc99..00000000 --- a/ci/expect_scripts/error-handling.exp +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)error-handling - -expect "NotFound\r\n" { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/file-accessed-modified-created-time.exp b/ci/expect_scripts/file-accessed-modified-created-time.exp deleted file mode 100644 index a63cd9dc..00000000 --- a/ci/expect_scripts/file-accessed-modified-created-time.exp +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)file-accessed-modified-created-time - -expect { - -re {LICENSE file time metadata:\r\n Modified: 20.*\r\n Accessed: 20.*\r\n Created: 20.*\r\n} { - expect eof { - check_exit_and_segfault - } - } - timeout { - puts stderr "\nExpect script failed: timed out waiting for expected output." - exit 1 - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/file-permissions.exp b/ci/expect_scripts/file-permissions.exp deleted file mode 100644 index 4c15e1cb..00000000 --- a/ci/expect_scripts/file-permissions.exp +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)file-permissions - -set expected_output [normalize_output { -LICENSE file permissions: - Executable: Bool.false - Readable: Bool.true - Writable: Bool.true -}] - -expect $expected_output { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/file-read-buffered.exp b/ci/expect_scripts/file-read-buffered.exp deleted file mode 100644 index ab83a72e..00000000 --- a/ci/expect_scripts/file-read-buffered.exp +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -spawn $env(EXAMPLES_DIR)file-read-buffered - -expect "Done reading file: {bytes_read: 1915, lines_read: 17}\r\n" { - expect eof { - check_exit_and_segfault - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 diff --git a/ci/expect_scripts/file-read-write.exp b/ci/expect_scripts/file-read-write.exp deleted file mode 100755 index be666a4a..00000000 --- a/ci/expect_scripts/file-read-write.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-read-write - - -set expected_output [normalize_output { -Writing a string to out.txt -I read the file back. Its contents are: "a string!" -}] - -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/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/hello-world.exp b/ci/expect_scripts/hello-world.exp deleted file mode 100644 index 8e5fa246..00000000 --- a/ci/expect_scripts/hello-world.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)hello-world - -expect "Hello, World!\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/http.exp b/ci/expect_scripts/http.exp deleted file mode 100644 index d5cc2f0a..00000000 --- a/ci/expect_scripts/http.exp +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -source ./ci/expect_scripts/shared-code.exp - -# Start server to test with in the background and capture its process ID -set server_pid [exec ./ci/rust_http_server/target/release/rust_http_server &] -sleep 3 - -spawn $env(EXAMPLES_DIR)http - -expect "I received 'Hello utf8' from the server.\r\n" { - expect "The json I received was: { foo: \"Hello Json!\" }\r\n" { - - # we can kill our rust server now - exec kill $server_pid - - expect "\r\n" { - expect "\r\n" { - expect eof { - check_exit_and_segfault - } - } - } - } -} - -exec kill $server_pid - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file diff --git a/ci/expect_scripts/locale.exp b/ci/expect_scripts/locale.exp deleted file mode 100644 index 61b9eb64..00000000 --- a/ci/expect_scripts/locale.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)locale - -set expected_output [normalize_output { -The most preferred locale for this system or application: [a-zA-Z\-]+ -All available locales for this system or application: \[.*\] -}] - -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 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 deleted file mode 100644 index 2d165d26..00000000 --- a/ci/expect_scripts/path.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 - -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 -}] - -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/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 deleted file mode 100644 index a6073fad..00000000 --- a/ci/expect_scripts/random.exp +++ /dev/null @@ -1,24 +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)random - -set expected_output [normalize_output " -Random U64 seed is: \\\d+ -Random U32 seed is: \\\d+ -"] - -expect -re $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/shared-code.exp b/ci/expect_scripts/shared-code.exp deleted file mode 100644 index cbdc14a4..00000000 --- a/ci/expect_scripts/shared-code.exp +++ /dev/null @@ -1,24 +0,0 @@ -proc normalize_output {output} { - # Remove leading newline for cleaner multiline string formatting - regsub {^\n} $output "" result - # Convert Unix line endings to Windows-style line endings for expect matching - regsub -all {\n} $result "\r\n" result - return $result -} - -proc check_exit_and_segfault {} { - set status [wait] - set exit_code [lindex $status 2] - - if {$exit_code != 0} { - puts stderr "\nExpect script failed: The roc executable exited with a non-zero exit code: $exit_code." - exit 1 - } else { - if {[string first "SIGSEGV" $status] != -1} { - puts stderr "\nExpect script failed: The roc executable experienced a segmentation fault." - exit 1 - } else { - exit 0 - } - } -} \ No newline at end of file diff --git a/ci/expect_scripts/sqlite-basic.exp b/ci/expect_scripts/sqlite-basic.exp deleted file mode 100644 index 9f9107a8..00000000 --- a/ci/expect_scripts/sqlite-basic.exp +++ /dev/null @@ -1,30 +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)todos.db - -spawn $env(EXAMPLES_DIR)sqlite-basic - - -set expected_output [normalize_output { -All Todos: - id: 3, task: Share my ❤️ for Roc, status: Todo - -Completed Todos: - id: 1, task: Prepare for AoC, status: Completed -}] - -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/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-basic.exp b/ci/expect_scripts/stdin-basic.exp deleted file mode 100644 index 751feb30..00000000 --- a/ci/expect_scripts/stdin-basic.exp +++ /dev/null @@ -1,28 +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)stdin-basic - - -expect "What's your first name?\r\n" - -send -- "John\r" - -expect "What's your last name?\r\n" - -send -- "Doe\r" - -expect "Hi, John Doe! 👋\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/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 deleted file mode 100644 index e0495f2d..00000000 --- a/ci/expect_scripts/tcp-client.exp +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/expect - -# uncomment line below for debugging -# exp_internal 1 - -set timeout 7 - -# 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(EXAMPLES_DIR)tcp-client - - -expect "Connected!\r\n" { - expect "> " { - send -- "Hi\r" - expect "< Hi\r\n" { - exit 0 - } - } -} - -puts stderr "\nExpect script failed: output was different from expected value. uncomment `exp_internal 1` to debug." -exit 1 diff --git a/ci/expect_scripts/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/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 deleted file mode 100644 index 3912ed40..00000000 --- a/ci/expect_scripts/time.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)time - -expect -re "Completed in \[0-9]+ ns\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/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/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..28edb946 100644 --- a/ci/rust_http_server/Cargo.toml +++ b/ci/rust_http_server/Cargo.toml @@ -2,6 +2,7 @@ name = "rust_http_server" version = "0.1.0" edition = "2021" +rust-version = "1.83" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,6 +11,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..d804c1ad 100644 --- a/ci/rust_http_server/src/main.rs +++ b/ci/rust_http_server/src/main.rs @@ -1,16 +1,15 @@ /// Adapted from hyper examples examples/hello.rs and examples/web_api.rs /// Licensed under the MIT License. /// Thank you, hyper contributors! - use bytes::Bytes; +use http_body_util::{BodyExt, Full}; use hyper::body::Incoming; -use std::net::SocketAddr; -use hyper::{Method, Request, Response, StatusCode}; -use hyper::service::{service_fn}; use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Method, Request, Response, StatusCode}; use hyper_util::rt::TokioTimer; +use std::net::SocketAddr; use tokio::net::TcpListener; -use http_body_util::{BodyExt, Full}; type GenericError = Box; type BoxBody = http_body_util::combinators::BoxBody; @@ -26,16 +25,40 @@ async fn handle_request(req: Request) -> Result, Gen (&Method::GET, "/utf8test") => { // UTF-8 encoded "Hello utf8" let utf8_bytes = "Hello utf8".as_bytes().to_vec(); - + Response::builder() .status(StatusCode::OK) .header("Content-Type", "text/plain; charset=utf-8") .body(full(Bytes::from(utf8_bytes)))? - }, + } + (&Method::GET, "/html") => Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "text/html; charset=utf-8") + .body(full(Bytes::from_static( + b"\nBasic CLI HTTP test\n", + )))?, + (&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])))?, + (&Method::POST, "/echo-json") => { + let body = req.into_body().collect().await?.to_bytes(); + + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(full(body))? + } _ => { - // Default response (original functionality) - // output of: Encode.to_bytes({foo: "Hello Json!"}, Json.utf8) - 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]; + // 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() .status(StatusCode::OK) 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..74960733 --- /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. scripts/build.py 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..738e719a --- /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. scripts/build.py 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/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..e424eb8a 100644 --- a/examples/bytes-stdin-stdout.roc +++ b/examples/bytes-stdin-stdout.roc @@ -1,15 +1,18 @@ +## Copy raw bytes from standard input to standard output and report the total. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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(OsStr) => Try({}, _) +main! = |_args| { + data = Stdin.read_to_end!()? -main! : List Arg => Result {} _ -main! = |_args| - data = Stdin.bytes!({})? - Stderr.write_bytes!(data)? - Stdout.write_bytes!(data)? - Ok {} + Stdout.write_bytes!(data)? + + Stderr.line!("Copied ${data.len().to_str()} bytes from stdin to stdout.")? + + Ok({}) +} diff --git a/examples/command-line-args.roc b/examples/command-line-args.roc index 44285b62..e821ef30 100644 --- a/examples/command-line-args.roc +++ b/examples/command-line-args.roc @@ -1,37 +1,37 @@ -app [main!] { - pf: platform "../platform/main.roc", -} +## Read native command-line arguments without losing non-Unicode data. +app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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(OsStr) => Try({}, _) +main! = |args| { + # Skip first arg (executable path), get the remaining args + match args.drop_first(1) { + [first_arg, ..] => { + + Stdout.line!("received argument: ${OsStr.display(first_arg)}")? + + match OsStr.to_raw(first_arg) { + Utf8(str) => { + Stdout.line!("UTF-8 argument text: ${Str.inspect(str)}")? + round_tripped_arg = OsStr.from_raw(Utf8(str)) + Stdout.line!("back to OsStr: ${Str.inspect(round_tripped_arg)}")? + } + UnixBytes(bytes) => { + Stdout.line!("Unix argument, bytes: ${Str.inspect(bytes)}")? + round_tripped_arg = OsStr.from_raw(UnixBytes(bytes)) + Stdout.line!("back to OsStr: ${Str.inspect(round_tripped_arg)}")? + } + WindowsU16s(u16s) => { + Stdout.line!("Windows argument, UTF-16 code units: ${Str.inspect(u16s)}")? + round_tripped_arg = OsStr.from_raw(WindowsU16s(u16s)) + Stdout.line!("back to OsStr: ${Str.inspect(round_tripped_arg)}")? + } + } + + Ok({}) + } + [] => Err(MissingArgument) + } +} diff --git a/examples/command.roc b/examples/command.roc index 1aaa362b..3b343ab6 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -1,51 +1,43 @@ +## Run child processes, capture their output, and configure their environment. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout import pf.Cmd -import pf.Arg exposing [Arg] -# Different ways to run commands like you do in a terminal. +main! : List(OsStr) => Try({}, _) +main! = |_args| { + # Simplest way to execute a command (prints to your terminal). + Cmd.exec!("echo", ["Hello"]) ? |err| ExecEchoFailed(err) -# To run this example: check the README.md in this folder + # To execute and capture the output (stdout and stderr) without inheriting your terminal. + cmd_output = Cmd.new("echo") + .args(["Hi"]) + .exec_output!() ? |err| ExecOutputEchoFailed(err) -main! : List Arg => Result {} _ -main! = |_args| + Stdout.line!("${Str.inspect(cmd_output)}")? - # Simplest way to execute a command (prints to your terminal). - Cmd.exec!("echo", ["Hello"])? + # To run a command with a controlled environment. + env_cmd = Cmd.new("/usr/bin/env") + .args(["-i", "BAZ=DUCK", "FOO=BAR", "XYZ=ABC"]) + Stdout.line!("Command config: ${Str.inspect(env_cmd)}")? + env_cmd.exec_cmd!() ? |err| ExecCmdEnvFailed(err) - # To execute and capture the output (stdout and stderr) without inheriting your terminal. - cmd_output = - Cmd.new("echo") - |> Cmd.args(["Hi"]) - |> Cmd.exec_output!()? + # To 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!() ? |err| ExecExitCodeCatFailed(err) - Stdout.line!("${Inspect.to_str(cmd_output)}")? + Stdout.line!("Exit code: ${exit_code.to_str()}")? - # 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 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!() ? |err| ExecOutputBytesEchoFailed(err) - # 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!("${Str.inspect(cmd_output_bytes)}")? - 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({}) + Ok({}) +} diff --git a/examples/dir.roc b/examples/dir.roc index a166520a..1a31915e 100644 --- a/examples/dir.roc +++ b/examples/dir.roc @@ -1,42 +1,49 @@ +## Create, inspect, and clean up a small directory tree. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout -import pf.Dir import pf.Path -import pf.File -import pf.Arg exposing [Arg] -# Demo of all Dir functions. +main! : List(OsStr) => Try({}, _) +main! = |_args| { + # Best-effort cleanup from a previous interrupted run. + workspace : Path + workspace = "demo-workspace" -# To run this example: check the README.md in this folder + workspace.delete_all!() ?? {} -main! : List Arg => Result {} _ -main! = |_args| + # Create a directory + workspace.create_dir!()? - # Create a directory - Dir.create!("empty-dir")? + # Create a directory and its parents + components : Path + components = "demo-workspace/src/components" - dir_exists = File.is_dir!("empty-dir")? - expect dir_exists + components.create_all!()? - # Create a directory and its parents - Dir.create_all!("nested-dir/a/b/c")? + # Create a child directory + assets : Path + assets = "demo-workspace/assets" - # Create a child directory - Dir.create!("nested-dir/child")? + assets.create_dir!()? - # List the contents of a directory - paths_as_str = - Dir.list!("nested-dir") - |> Result.map_ok(|paths| List.map(paths, Path.display))? + # List the contents of a directory + paths = workspace.list!()? - # Check the contents of the directory - expect Set.from_list(paths_as_str) == Set.from_list(["nested-dir/a", "nested-dir/child"]) + displayed = paths.map(|path| path.display()) - # Delete an empty directory - Dir.delete_empty!("empty-dir")? + # Check the contents of the directory + expect paths.len() == 2 + expect displayed.contains("demo-workspace/src") + expect displayed.contains("demo-workspace/assets") - # Delete all directories recursively - Dir.delete_all!("nested-dir")? + Stdout.line!("Workspace entries: ${Str.join_with(displayed, ", ")}")? - Stdout.line!("Success!") + # Delete all directories recursively + workspace.delete_all!()? + + Stdout.line!("Workspace cleaned up.")? + + Ok({}) +} diff --git a/examples/env-var.roc b/examples/env-var.roc index a2b99e54..9b2f42f2 100644 --- a/examples/env-var.roc +++ b/examples/env-var.roc @@ -1,25 +1,21 @@ +## Read native and UTF-8 environment variables. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout import pf.Env -import pf.Arg exposing [Arg] -# How to read environment variables with Env.decode +main! : List(OsStr) => Try({}, _) +main! = |_args| { + editor = Env.var!("EDITOR")? -# To run this example: check the README.md in this folder + Stdout.line!("Your favorite editor is ${editor.display()}!")? -main! : List Arg => Result {} _ -main! = |_args| + letters = Env.var_str!("LETTERS")? - editor = Env.decode!("EDITOR")? + joined_letters = Str.join_with(letters.split_on(","), " ") - Stdout.line!("Your favorite editor is ${editor}!")? + Stdout.line!("Your favorite letters are: ${joined_letters}")? - # 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}") + Ok({}) +} diff --git a/examples/error-handling.roc b/examples/error-handling.roc index 73261ff9..65fe836b 100644 --- a/examples/error-handling.roc +++ b/examples/error-handling.roc @@ -1,30 +1,55 @@ +## Handle common filesystem errors with tag-pattern matching. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout -import pf.Stderr -import pf.File -import pf.Arg exposing [Arg] +import pf.Path -# To run this example: check the README.md in this folder +main! : List(OsStr) => Try({}, _) +main! = |_args| { -# Demonstrates handling of every possible error + file_name : Path + file_name = "test-file.txt" -main! : List Arg => Result {} _ -main! = |_args| - file_name = "test-file.txt" + # Try to read a file that doesn't exist - should error + missing_file : Path + missing_file = "nonexistent-file.txt" - file_read_result = File.read_utf8!(file_name) + match missing_file.read_utf8!() { + Ok(content) => Err(UnexpectedReadSuccess(content))? + Err(PathErr(NotFound)) => Stdout.line!("Expected error: Path not found (NotFound)")? + Err(PathErr(PermissionDenied)) => Stdout.line!("Error: Permission denied")? + Err(PathErr(Other(msg))) => Stdout.line!("Error: ${msg}")? + Err(_) => Stdout.line!("Error: Other file error")? + } - when file_read_result is - Ok(file_content) -> - Stdout.line!("${file_name} contatins: ${file_content}") + # Filesystem kind mismatches are portable typed errors. + directory : Path + directory = "examples" - 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)}" + match directory.read_bytes!() { + Err(PathErr(IsADirectory)) => Stdout.line!("Expected error: Path is a directory (IsADirectory)")? + Ok(_) => Err(UnexpectedDirectoryReadSuccess)? + Err(err) => Err(UnexpectedDirectoryReadError(err))? + } - - Stderr.line!(err_msg)? - Err(Exit(1, "")) # non-zero exit code to indicate failure \ No newline at end of file + regular_file : Path + regular_file = "LICENSE" + + match regular_file.list!() { + Err(PathErr(NotADirectory)) => Stdout.line!("Expected error: Path is not a directory (NotADirectory)")? + Ok(_) => Err(UnexpectedFileListSuccess)? + Err(err) => Err(UnexpectedFileListError(err))? + } + + file_name.write_utf8!("Hello from error-handling example!") ? |err| FileWriteFailed(err) + + content = file_name.read_utf8!() ? |err| FileReadFailed(err) + + # Cleanup + file_name.delete!() ? |err| FileDeleteFailed(err) + + Stdout.line!("${file_name.display()} contains: ${content}")? + + Ok({}) +} diff --git a/examples/file-accessed-modified-created-time.roc b/examples/file-accessed-modified-created-time.roc index cb2452aa..99500b56 100644 --- a/examples/file-accessed-modified-created-time.roc +++ b/examples/file-accessed-modified-created-time.roc @@ -1,31 +1,42 @@ +## Read a file's accessed, modified, and creation timestamps. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout -import pf.File +import pf.Path import pf.Utc -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder +main! : List(OsStr) => Try({}, _) +main! = |args| { -main! : List Arg => Result {} _ -main! = |_args| - file = "LICENSE" + file : Path + file = path_argument(args)? - # 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_millis_since_epoch(file.time_modified!()?) - time_modified = Utc.to_iso_8601(File.time_modified!(file)?) + time_accessed = Utc.to_millis_since_epoch(file.time_accessed!()?) - time_accessed = Utc.to_iso_8601(File.time_accessed!(file)?) + created_line = match file.time_created!() { + Ok(time_created) => " Created: ${Utc.to_millis_since_epoch(time_created).to_str()} ms since epoch" + Err(PathErr(Unsupported)) => " Created: unsupported" + Err(err) => Err(err)? + } - time_created = Utc.to_iso_8601(File.time_created!(file)?) + Stdout.line!( + \\${file.display()} file time metadata: + \\ Modified: ${time_modified.to_str()} ms since epoch + \\ Accessed: ${time_accessed.to_str()} ms since epoch + \\${created_line} + , + )? + Ok({}) +} - Stdout.line!( - """ - ${file} file time metadata: - Modified: ${time_modified} - Accessed: ${time_accessed} - Created: ${time_created} - """ - ) \ No newline at end of file +## Parse the first argument into a Path +path_argument : List(OsStr) -> Try(Path, _) +path_argument = |args| + match args.drop_first(1) { + [first, ..] => Ok(Path.from_os_str(first)) + [] => Err(MissingPathArgument) + } diff --git a/examples/file-permissions.roc b/examples/file-permissions.roc index e2b0c2ee..70b4a276 100644 --- a/examples/file-permissions.roc +++ b/examples/file-permissions.roc @@ -1,26 +1,35 @@ +## Inspect whether a file is executable, readable, and writable. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout -import pf.File -import pf.Arg exposing [Arg] +import pf.Path -# To run this example: check the README.md in this folder +main! : List(OsStr) => Try({}, _) +main! = |args| { -main! : List Arg => Result {} _ -main! = |_args| - file = "LICENSE" + file : Path + file = path_argument(args)? - is_executable = File.is_executable!(file)? + is_executable = file.is_executable!()? + is_readable = file.is_readable!()? + is_writable = file.is_writable!()? - is_readable = File.is_readable!(file)? + Stdout.line!( + \\${file.display()} file permissions: + \\ Executable: ${Str.inspect(is_executable)} + \\ Readable: ${Str.inspect(is_readable)} + \\ Writable: ${Str.inspect(is_writable)} + , + )? - is_writable = File.is_writable!(file)? + Ok({}) +} - 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 +## Parse the first argument into a Path +path_argument : List(OsStr) -> Try(Path, _) +path_argument = |args| + match args.drop_first(1) { + [first, ..] => Ok(Path.from_os_str(first)) + [] => Err(MissingPathArgument) + } diff --git a/examples/file-read-buffered.roc b/examples/file-read-buffered.roc index 3801760e..e06b1448 100644 --- a/examples/file-read-buffered.roc +++ b/examples/file-read-buffered.roc @@ -1,52 +1,42 @@ +## Read a file incrementally with a buffer instead of loading it all at once. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout import pf.File -import pf.Arg exposing [Arg] - -# To run this example: check the README.md in this folder - -# # Buffered File Reading -# -# Instead of reading an entire file and storing all of it in memory, -# like with File.read_utf8, you may want to read it in parts. -# A part of the file is stored in a buffer. -# Typically you process a part and then you ask for the next one. -# -# This can be useful to process large files without using a lot of RAM or -# requiring the user to wait until the complete file is processed when they -# only wanted to look at the first page. -# -# 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)}") - -ReadSummary : { - lines_read : U64, - bytes_read : U64, +import pf.Path + +main! : List(OsStr) => Try({}, _) +main! = |_args| { + + reader : File.Reader + reader = File.open_reader!("LICENSE")? + + read_summary : ReadSummary + read_summary = process_line!(reader, { lines_read: 0, bytes_read: 0 })? + + Stdout.line!("Done reading file: ${Str.inspect(read_summary)}")? + + Ok({}) } +ReadSummary := { lines_read : U64, bytes_read : U64 } + ## 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 -> - Ok({ lines_read, bytes_read }) - - Ok(bytes) -> - process_line!( - reader, - { - lines_read: lines_read + 1, - bytes_read: bytes_read + (List.len(bytes) |> Num.int_cast), - }, - ) - - Err(err) -> - Err(ErrorReadingLine(Inspect.to_str(err))) + match reader.read_line!() { + Ok(bytes) if bytes.len() == 0 => + Ok({ lines_read, bytes_read }) + + Ok(bytes) => + process_line!( + reader, + { + lines_read: lines_read + 1, + bytes_read: bytes_read + bytes.len(), + }, + ) + + Err(err) => Err(err) + } diff --git a/examples/file-read-write.roc b/examples/file-read-write.roc index 13d60cdb..24c1df57 100644 --- a/examples/file-read-write.roc +++ b/examples/file-read-write.roc @@ -1,31 +1,26 @@ +## Write a UTF-8 file, read it back, and delete it. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout -import pf.File -import pf.Arg exposing [Arg] +import pf.Path -# To run this example: check the README.md in this folder +main! : List(OsStr) => Try({}, _) +main! = |_args| { -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 - out_file = "out.txt" + out_file : Path + out_file = "out.txt" - file_write_read!(out_file)? + Stdout.line!("Writing a string to out.txt")? - # Cleanup - File.delete!(out_file) + out_file.write_utf8!("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 = out_file.read_utf8!()? + # Cleanup + out_file.delete!()? + Stdout.line!("I read the file back. Its contents are: \"${contents}\"")? + Ok({}) +} diff --git a/examples/file-size.roc b/examples/file-size.roc index 854ff76a..b8709fd2 100644 --- a/examples/file-size.roc +++ b/examples/file-size.roc @@ -1,13 +1,27 @@ +## Report the size in bytes of a path supplied on the command line. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout -import pf.File -import pf.Arg exposing [Arg] +import pf.Path -# To run this example: check the README.md in this folder +main! : List(OsStr) => Try({}, _) +main! = |args| { -main! : List Arg => Result {} _ -main! = |_args| - file_size = File.size_in_bytes!("LICENSE")? + file : Path + file = path_argument(args)? - Stdout.line!("The size of the LICENSE file is: ${Num.to_str(file_size)} bytes") + file_size : U64 + file_size = file.size_in_bytes!()? + + Stdout.line!("${file.display()} is ${file_size.to_str()} bytes")? + + Ok({}) +} + +path_argument : List(OsStr) -> Try(Path, [MissingPathArgument, ..]) +path_argument = |args| + match args.drop_first(1) { + [first, ..] => Ok(Path.from_os_str(first)) + [] => Err(MissingPathArgument) + } diff --git a/examples/hello-world.roc b/examples/hello-world.roc index a4c7ec98..be350a7c 100644 --- a/examples/hello-world.roc +++ b/examples/hello-world.roc @@ -1,10 +1,10 @@ +## Print a minimal greeting to standard output. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder - -main! : List Arg => Result {} _ -main! = |_args| - Stdout.line!("Hello, World!") +main! = |_args| { + Stdout.line!("Hello, World!")? + Ok({}) +} diff --git a/examples/hello.roc b/examples/hello.roc new file mode 100644 index 00000000..88846766 --- /dev/null +++ b/examples/hello.roc @@ -0,0 +1,23 @@ +## Greet a name supplied as a native command-line argument. +app [main!] { pf: platform "../platform/main.roc" } + +import pf.OsStr +import pf.Stdout + +main! : List(OsStr) => Try({}, _) +main! = |args| { + + name : Str + name = greeting_name(args) + + Stdout.line!("Hello, ${name}, from basic-cli!")? + + Ok({}) +} + +greeting_name : List(OsStr) -> Str +greeting_name = |args| + match args.drop_first(1) { + [first, ..] => OsStr.display(first) + [] => "friend" + } diff --git a/examples/http-client.roc b/examples/http-client.roc new file mode 100644 index 00000000..57d02c3a --- /dev/null +++ b/examples/http-client.roc @@ -0,0 +1,86 @@ +## Send HTTP requests and handle UTF-8, JSON, and transport failures. +app [main!] { + pf: platform "../platform/main.roc", + http: "https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst", +} + +import pf.OsStr +import pf.Http +import pf.Stdout +import http.Request +import http.Response + +# 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(OsStr) => Try({}, _) +main! = |_args| run_demo!() + +run_demo! : () => Try({}, _) +run_demo! = || { + utf8 = Http.get_utf8!("http://127.0.0.1:9000/utf8test") ? |err| GetUtf8Failed(err) + write_line!("I received '${utf8}' from the server.")? + + request = Request.from_method(GET).with_uri("http://127.0.0.1:9000/utf8test") + response = Http.send!(request) ? |err| SendFailed(err) + status = U16.to_str(Response.status(response)) + + decoded : { foo : Str } + decoded = Http.get!("http://127.0.0.1:9000") ? |err| GetJsonFailed(err) + + echo_request = Request.from_method(POST).with_uri("http://127.0.0.1:9000/echo-json") + echo_response = Http.send_json!(echo_request, { foo: "Hello Json!" }) ? |err| SendJsonFailed(err) + echoed : { foo : Str } + echoed = Http.decode_json_response(echo_response) ? |err| EchoedJsonDecodeFailed(err) + + write_line!("The json I received was: { foo: \"${decoded.foo}\" }")? + write_line!("send! returned status ${status}.")? + write_line!("send_json! echoed: { foo: \"${echoed.foo}\" }.")? + reject_invalid_json!()? + reject_invalid_utf8!()? + reject_invalid_url!()? + + Ok({}) +} + +reject_invalid_url! : () => Try({}, _) +reject_invalid_url! = || { + result = Http.send!(Request.from_method(GET).with_uri("ftp://example.com")) + + match result { + Err(InvalidUrl(UnsupportedScheme("ftp"))) => write_line!("invalid request URL was rejected.") + Err(err) => Err(InvalidUrlRejectedWithWrongError(err)) + Ok(_) => Err(InvalidUrlUnexpectedlySucceeded) + } +} + +reject_invalid_json! : () => Try({}, _) +reject_invalid_json! = || { + result : Try({ foo : Str }, _) + result = Http.get!("http://127.0.0.1:9000/invalid-json") + + match result { + Err(JsonErr(_)) => write_line!("invalid JSON was rejected.") + Err(err) => Err(InvalidJsonRejectedWithWrongError(err)) + Ok(_) => Err(InvalidJsonUnexpectedlySucceeded) + } +} + +reject_invalid_utf8! : () => Try({}, _) +reject_invalid_utf8! = || { + result = Http.get_utf8!("http://127.0.0.1:9000/invalid-utf8") + + match result { + Err(BadBody(_)) => write_line!("invalid UTF-8 was rejected.") + Err(err) => Err(InvalidUtf8RejectedWithWrongError(err)) + Ok(_) => Err(InvalidUtf8UnexpectedlySucceeded) + } +} + +write_line! : Str => Try({}, _) +write_line! = |message| Stdout.line!(message) diff --git a/examples/http.roc b/examples/http.roc index f25907f8..1e91e6a7 100644 --- a/examples/http.roc +++ b/examples/http.roc @@ -1,73 +1,37 @@ +## Fetch UTF-8, JSON, and HTML responses from a local HTTP server. app [main!] { - pf: platform "../platform/main.roc", - json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.13.0/RqendgZw5e1RsQa3kFhgtnMP8efWoqGRsAvubx4-zus.tar.br", + pf: platform "../platform/main.roc", + http: "https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst", } +import pf.OsStr import pf.Http import pf.Stdout -import json.Json -import pf.Arg exposing [Arg] +import http.Request +import http.Response -# Demo of all basic-cli Http functions +main! : List(OsStr) => Try({}, _) +main! = |_args| { -# 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. + hello_str = Http.get_utf8!("http://127.0.0.1:9000/utf8test") ? |err| GetUtf8Failed(err) + Stdout.line!("I received '${hello_str}' from the server.")? -main! : List Arg => Result {} _ -main! = |_args| + decoded : { foo : Str } + decoded = Http.get!("http://127.0.0.1:9000") ? |err| GetJsonFailed(err) - # # HTTP GET a String - # ---------------- + Stdout.line!("The json I received was: { foo: \"${decoded.foo}\" }")? - 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 + response = Http.send!(Request.from_method(GET).with_uri("http://127.0.0.1:9000/html")) ? |err| SendHtmlFailed(err) + body = Str.from_utf8(response.body()) ? |err| HtmlBodyUtf8Failed(err) - Stdout.line!("I received '${hello_str}' from the server.\n")? + Stdout.line!("Response body:")? + Stdout.line!(body)? - # # Getting json - # ------------ + response_2 = Http.send!(Request.from_method(GET).with_uri("http://127.0.0.1:9000/html")) ? |err| SendSecondHtmlFailed(err) + body_2 = Str.from_utf8(Response.body(response_2)) ? |err| SecondHtmlBodyUtf8Failed(err) - # We decode/deserialize the json `{ "foo": "something" }` into a Roc record + Stdout.line!("Response body 2:")? + Stdout.line!(body_2)? - { 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") + Ok({}) +} diff --git a/examples/locale.roc b/examples/locale.roc index fe151722..d7c0b5bc 100644 --- a/examples/locale.roc +++ b/examples/locale.roc @@ -1,18 +1,29 @@ +## Parse, encode, and inspect the system's preferred locales. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout -import pf.Arg exposing [Arg] import pf.Locale -# Getting the preferred locale and all available locales +main! : List(OsStr) => Try({}, _) +main! = |_args| { -# To run this example: check the README.md in this folder + example_locale : Locale + example_locale = "en-US" -main! : List Arg => Result {} _ -main! = |_args| - - locale_str = Locale.get!({})? - Stdout.line!("The most preferred locale for this system or application: ${locale_str}")? + Stdout.line!("Locale JSON example: ${Json.to_str(example_locale)}")? - 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 + locale_str = match Locale.get!() { + Ok(locale) => locale.to_str() + Err(NotAvailable) => "" + } + + Stdout.line!("The most preferred locale for this system or application: ${locale_str}")? + + all_locales = Locale.all!() + locales_str = Str.join_with(all_locales.map(|locale| locale.to_str()), ", ") + + Stdout.line!("All available locales for this system or application: [${locales_str}]")? + + Ok({}) +} diff --git a/examples/path.roc b/examples/path.roc index 123ef670..ed6267b8 100644 --- a/examples/path.roc +++ b/examples/path.roc @@ -1,28 +1,30 @@ +## Inspect a path's filename, extension, representation, and filesystem type. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr exposing [OsStr] import pf.Stdout import pf.Path -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder +main! : List(OsStr) => Try({}, _) +main! = |args| { + path = path_argument(args)? + filename = Path.filename(path).map_ok(Path.display) ?? "" + extension = Path.ext(path).map_ok(Path.display) ?? "" -# Demo of basic-cli Path functions + Stdout.line!( + \\Path: ${Path.display(path)} + \\Debug: ${Str.inspect(path)} + \\Filename: ${filename} + \\Extension: ${extension} + \\Type: ${Str.inspect(Path.type!(path)?)} + , + )? -main! : List Arg => Result {} _ -main! = |_args| + Ok({}) +} - 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)} - """ - ) +path_argument = |args| + match args.drop_first(1) { + [first, ..] => Ok(Path.from_os_str(first)) + [] => Err(MissingPathArgument) + } diff --git a/examples/print.roc b/examples/print.roc index 433de1c1..68e592dd 100644 --- a/examples/print.roc +++ b/examples/print.roc @@ -1,31 +1,28 @@ +## Write lines, text, and lists to standard output and standard error. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdout import pf.Stderr -import pf.Arg exposing [Arg] -# Printing to stdout and stderr +main! : List(OsStr) => Try({}, _) +main! = |_args| { + # Print a string to stdout + Stdout.line!("Hello, world!")? -# To run this example: check the README.md in this folder + # Print without a newline + Stdout.write!("No newline after me.")? -main! : List Arg => Result {} _ -main! = |_args| - - # # Print a string to stdout - Stdout.line!("Hello, world!")? + # Print a string to stderr + Stderr.line!("Hello, error!")? - # # Print without a newline - Stdout.write!("No newline after me.")? + # Print a string to stderr without a newline + Stderr.write!("Err with no newline after.")? - # # Print a string to stderr - Stderr.line!("Hello, error!")? + # Print a list to stdout + for str in ["Foo", "Bar", "Baz"] { + Stdout.line!(str)? + } - # # 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. + Ok({}) +} diff --git a/examples/random.roc b/examples/random.roc index b539f9c8..a2199785 100644 --- a/examples/random.roc +++ b/examples/random.roc @@ -1,20 +1,17 @@ +## Generate random 32-bit and 64-bit seed values. 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.OsStr 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)}")? +main! : List(OsStr) => Try({}, _) +main! = |_args| { + random_u64 = Random.seed_u64!()? + Stdout.line!("Random U64 seed is: ${random_u64.to_str()}")? - random_u32 = Random.random_seed_u32!({})? - Stdout.line!("Random U32 seed is: ${Inspect.to_str(random_u32)}") + random_u32 = Random.seed_u32!()? + Stdout.line!("Random U32 seed is: ${random_u32.to_str()}")? - # 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 + Ok({}) +} diff --git a/examples/sqlite-basic.roc b/examples/sqlite-basic.roc index f9c37db2..6450b755 100644 --- a/examples/sqlite-basic.roc +++ b/examples/sqlite-basic.roc @@ -1,13 +1,11 @@ +## Query a SQLite database and decode rows into application records. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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` - -# Demo of basic Sqlite usage +import pf.Path # Sql to create the table: # CREATE TABLE todos ( @@ -16,55 +14,76 @@ import pf.Arg exposing [Arg] # status TEXT NOT NULL # ); -main! : List Arg => Result {} _ -main! = |_args| - db_path = Env.var!("DB_PATH")? +main! : List(OsStr) => Try({}, _) +main! = |_args| { + db_path = match Env.var!("DB_PATH") { + Ok(p) => Path.from_os_str(p) + Err(_) => "./examples/todos.db" + } - todos = query_todos_by_status!(db_path, "todo")? + todos = query_todos_by_status!(db_path, "todo") ? |err| QueryTodosFailed(err) - Stdout.line!("All Todos:")? + print_line!("All Todos:")? - # print todos - List.for_each_try!( - todos, - |{ id, task, status }| - Stdout.line!("\tid: ${id}, task: ${task}, status: ${Inspect.to_str(status)}"), - )? + for todo in todos { + print_todo!(todo)? + } - completed_todos = query_todos_by_status!(db_path, "completed")? + completed_todos = query_todos_by_status!(db_path, "completed") ? |err| QueryCompletedTodosFailed(err) - Stdout.line!("\nCompleted Todos:")? - List.for_each_try!( - completed_todos, - |{ id, task, status }| - Stdout.line!("\tid: ${id}, task: ${task}, status: ${Inspect.to_str(status)}"), - ) + 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({}, _) +print_todo! = |todo| + print_line!(" id: ${todo.id}, task: ${todo.task}, status: ${status_to_str(todo.status)}") + +print_line! : Str => Try({}, _) +print_line! = |line| Stdout.line!(line) + 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), - }, - }, - ) + Sqlite.query_many!({ + path: db_path, + query: "SELECT id, task, status FROM todos WHERE status = :status;", + bindings: [{ name: ":status", value: String(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..374861e9 100644 --- a/examples/sqlite-everything.roc +++ b/examples/sqlite-everything.roc @@ -1,13 +1,11 @@ +## Exercise SQLite queries, decoders, nullable values, and prepared writes. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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 +import pf.Path # Sql that was used to create the table: # CREATE TABLE todos ( @@ -20,205 +18,238 @@ import pf.Arg exposing [Arg] # We recommend using `NOT NULL` when possible. # Note 2: boolean is "fake" in sqlite https://www.sqlite.org/datatype3.html -main! : List Arg => Result {} _ -main! = |_args| - db_path = Env.var!("DB_PATH")? - - # 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), - }, - })? - - Stdout.line!("All Todos:")? - - List.for_each_try!( - all_todos, - |{ id, task, status, edited }| - Stdout.line!("\tid: ${Num.to_str(id)}, task: ${task}, status: ${Inspect.to_str(status)}, edited: ${Inspect.to_str(edited)}"), - )? - - # Example: filter rows by status - - 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") - }, - )? - - Stdout.line!("\nIn-progress Todos:")? - - List.for_each_try!( - tasks_in_progress, - |task_description| - Stdout.line!("\tIn-progress tasks: ${task_description}"), - )? - - # 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) }, - ], - })? - - # 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 }, - { task: "Insert Roc list 3", status: Todo, edited: NotEdited }, - ] - - 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(", ") - - 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 - - Sqlite.execute!({ - path: db_path, - query: "INSERT INTO todos (task, status, edited) VALUES ${values_str};", - bindings: all_bindings, - })? - - # Example: update a row - - Sqlite.execute!({ - path: db_path, - query: "UPDATE todos SET status = :status WHERE task = :task;", - bindings: [ - { name: ":task", value: String("Make sql example.") }, - { name: ":status", value: encode_status(Completed) }, - ], - })? - - # Example: delete a row - - 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: count the number of rows - - count = Sqlite.query!({ - path: db_path, - query: "SELECT COUNT(*) as \"count\" FROM todos;", - bindings: [], - row: Sqlite.u64("count"), - })? - - expect count == 3 - - # 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), - }, - })? - - Stdout.line!("\nTodos sorted by length of task description:")? - - List.for_each_try!( - todos_sorted, - |{ task, status }| - Stdout.line!("\t task: ${task}, status: ${Inspect.to_str(status)}"), - )? - - Ok({}) +main! : List(OsStr) => Try({}, _) +main! = |_args| run!() + +run! : () => Try({}, _) +run! = || { + + # Read from environment variable, or use default + db_path = match Env.var!("DB_PATH") { + Ok(p) => Path.from_os_str(p) + Err(_) => "./examples/todos2.db" + } + + # Example: print all rows + all_todos = Sqlite.query_many!({ + path: db_path, + query: "SELECT * FROM todos;", + bindings: [], + rows: decode_full_todo, + }) ? |err| QueryAllTodosFailed(err) + + 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"), + }) ? |err| QueryInProgressTasksFailed(err) + + 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) }, + ], + }) ? |err| InsertTodoFailed(err) + + # Example: insert multiple rows from a Roc list + todos_list = [ + { task: "Insert Roc list 1", status: Todo, edited: NotEdited }, + { task: "Insert Roc list 2", status: Todo, edited: NotEdited }, + { task: "Insert Roc list 3", status: Todo, edited: NotEdited }, + ] + + values_str = Str.join_with( + todos_list.map_with_index( + |_t, indx| { + i = U64.to_str(indx) + "(:task${i}, :status${i}, :edited${i})" + }, + ), + ", ", + ) + + 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 = 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, + }) ? |err| InsertTodoListFailed(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) }, + ], + }) ? |err| UpdateTodoFailed(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| DeleteTodoFailed(err) + + # 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| CleanupInsertedTodosFailed(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| CountTodosFailed(err) + + print_line!("")? + print_line!("Row count: ${U64.to_str(count)}")? + + # Example: prepared statements + # Note: This is faster if you execute the same prepared statement many times. + prepared_update = Sqlite.prepare!({ + path: db_path, + query: "UPDATE todos SET status = status WHERE id = :id;", + }) ? |err| PrepareUpdateTodoFailed(err) + + prepared_update.execute!([{ name: ":id", value: Integer(1) }]) ? |err| ExecutePreparedUpdateFailed(err) + # Reuse verifies that execution resets the statement before returning. + prepared_update.execute!([{ name: ":id", value: Integer(2) }]) ? |err| ReusePreparedUpdateFailed(err) + + prepared_count = Sqlite.prepare!({ + path: db_path, + query: "SELECT COUNT(*) as \"count\" FROM todos;", + }) ? |err| PrepareCountTodosFailed(err) + prepared_count_value = prepared_count.query!([], Sqlite.u64("count")) ? |err| QueryPreparedCountFailed(err) + expect prepared_count_value == count + + prepared_query = Sqlite.prepare!({ + path: db_path, + # sort by the length of the task description + query: "SELECT * FROM todos ORDER BY LENGTH(task);", + }) ? |err| PrepareSortedTodosFailed(err) + + todos_sorted = prepared_query.query_many!([], decode_task_status) ? |err| QuerySortedTodosFailed(err) + # Reuse verifies that querying resets the statement before returning. + todos_sorted_again = prepared_query.query_many!([], decode_task_status) ? |err| ReuseSortedTodosFailed(err) + expect todos_sorted_again.len() == todos_sorted.len() + + 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({}, _) +print_line! = |s| Stdout.line!(s) + +# Decode every column of the todos table. The nullable `edited` column is returned +# raw (`[NotNull(I64), Null]`) and interpreted by `decode_edited` at the call site: +# decoding both `status` (via `?`) and `edited` inside this nested decoder lambda +# currently panics the type checker, so we keep only one interpreting `?` here. +decode_full_todo = |cols| + |stmt| { + id = Sqlite.i64("id")(cols)(stmt)? + task = Sqlite.str("task")(cols)(stmt)? + status_str = Sqlite.str("status")(cols)(stmt)? + 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..9739bcda 100644 --- a/examples/stdin-basic.roc +++ b/examples/stdin-basic.roc @@ -1,23 +1,18 @@ +## Prompt for two lines of standard input and print a greeting. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr import pf.Stdin import pf.Stdout -import pf.Arg exposing [Arg] -# To run this example: check the README.md in this folder +main! : List(OsStr) => Try({}, _) +main! = |_args| { + Stdout.line!("What's your first name?")? + first = Stdin.line!() ? |_| MissingFirstName -# 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?")? + last = Stdin.line!() ? |_| MissingLastName -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)")? + Ok({}) +} diff --git a/examples/stdin-pipe.roc b/examples/stdin-pipe.roc index 80d18140..91cd52ae 100644 --- a/examples/stdin-pipe.roc +++ b/examples/stdin-pipe.roc @@ -1,19 +1,20 @@ +## Read piped bytes to end-of-input and validate them as UTF-8. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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(OsStr) => Try({}, _) +main! = |_| { + # Data is only sent with Stdin.line! if the user presses Enter, + # so you'll need to use read_to_end! to read data that was piped in without a newline. + piped_in = Stdin.read_to_end!()? + piped_in_str = Str.from_utf8(piped_in)? - # Data is only sent with Stdin.line! if the user presses Enter, - # so you'll need to use read_to_end! to read data that was piped in without a newline. - piped_in = Stdin.read_to_end!({})? - piped_in_str = Str.from_utf8(piped_in)? + Stdout.line!("This is what you piped in: \"${piped_in_str}\"")? - 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..2b7009a8 100644 --- a/examples/tcp-client.roc +++ b/examples/tcp-client.roc @@ -1,78 +1,60 @@ +## Exchange lines with a local TCP echo server using buffered stream operations. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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 {} _ -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)}") +# To try it interactively, start an echo server in another terminal first: +# +# $ ncat -e $(which cat) -l 8085 +# +# then run this example. +main! : List(OsStr) => Try({}, _) +main! = |_args| { + + stream : Tcp.Stream + stream = Tcp.connect!("127.0.0.1", 8085) ? |err| ConnectFailed(err) + + verify_stream_methods!(stream)? + + Stdout.line!("Connected!")? + + run!(stream) +} + +## Exercise every read and write operation against the echo test server. +verify_stream_methods! : Tcp.Stream => Try({}, _) +verify_stream_methods! = |stream| { + stream.write!([1, 2, 3])? + exact_bytes = stream.read_exactly!(3)? + expect exact_bytes == [1, 2, 3] + + stream.write_utf8!("until|")? + until_bytes = stream.read_until!(124)? + expect until_bytes == [117, 110, 116, 105, 108, 124] + + stream.write!([42])? + up_to_bytes = stream.read_up_to!(1)? + expect up_to_bytes == [42] + + Ok({}) +} + +## Read a line from stdin, send it to the server, print the response, repeat. +run! : Tcp.Stream => Try({}, _) +run! = |stream| { + Stdout.write!("> ")? + match Stdin.line!() { + # No more input — exit cleanly. + Err(EndOfFile) => Ok({}) + Err(StdinErr(err)) => Err(StdinReadFailed(err)) + Ok(out_msg) => { + stream.write_utf8!("${out_msg}\n") ? |err| TcpWriteFailed(err) + in_msg = stream.read_line!() ? |err| TcpReadFailed(err) + Stdout.line!("< ${in_msg}")? + run!(stream) + } + } +} diff --git a/examples/temp-dir.roc b/examples/temp-dir.roc index fc4e7c05..4857032b 100644 --- a/examples/temp-dir.roc +++ b/examples/temp-dir.roc @@ -1,20 +1,18 @@ +## Print the operating system's default temporary directory. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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 +main! : List(OsStr) => Try({}, _) +main! = |_args| { -# Prints the default temp dir -# -# !! this requires the flag `--linker=legacy`: -# for example: `roc build examples/temp-dir.roc --linker=legacy` + temp_dir_path : Path + temp_dir_path = Env.temp_dir!() -main! : List Arg => Result {} _ -main! = |_args| + Stdout.line!("The temp dir path is ${temp_dir_path.display()}")? - temp_dir_path_str = Path.display(Env.temp_dir!({})) - - Stdout.line!("The temp dir path is ${temp_dir_path_str}") + Ok({}) +} diff --git a/examples/terminal-app-snake.roc b/examples/terminal-app-snake.roc index 2a593553..1a8555cd 100644 --- a/examples/terminal-app-snake.roc +++ b/examples/terminal-app-snake.roc @@ -1,230 +1,255 @@ +## Build a small full-screen snake game using terminal raw mode. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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 +Position : { x : I64, y : I64 } -# 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. +Snake : { first : Position, rest : List(Position) } -Position : { x : I64, y : I64 } +Direction : { dx : I64, dy : I64 } GameState : { - snake_lst : NonEmptyList, - food_pos : Position, - direction : [Up, Down, Left, Right], - game_over : Bool, + 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 } -main! : List Arg => Result {} _ -main! = |_args| - Tty.enable_raw_mode!({}) +left : Direction +left = { dx: -1, dy: 0 } - game_loop!(initial_state)? +right : Direction +right = { dx: 1, dy: 0 } - Tty.disable_raw_mode!({}) - Stdout.line!("\n--- Game Over ---") +init_snake_len : U64 +init_snake_len = snake_len(initial_state.snake) -game_loop! : GameState => Result {} _ -game_loop! = |state| - if state.game_over then - Ok({}) - else - draw_game!(state)? +main! : List(OsStr) => Try({}, _) +main! = |args| { + Tty.enable_raw_mode!() + game_result = game_loop!(initial_state_from_os_strs(args)) + Tty.disable_raw_mode!() - # Check keyboard input - input_bytes = Stdin.bytes!({})? + game_result? + Stdout.line!("\n--- Game Over ---")? + Ok({}) +} + +initial_state_from_os_strs : List(OsStr) -> GameState +initial_state_from_os_strs = |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) } +} - 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 +game_loop! : GameState => Try({}, _) +game_loop! = |state| { + if state.game_over { + Ok({}) + } else { + draw_game!(state)? - new_state = update_game(partial_new_state) - game_loop!(new_state) + input_bytes = Stdin.bytes!()? + new_state = update_game(apply_input(state, input_bytes)) + + 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 - 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 } +update_game = |state| { + if state.game_over { + state + } else { + new_head = move_head(state.snake.first, state.direction) + + if hit_wall(new_head) or snake_contains(state.snake, new_head) { + { ..state, game_over: Bool.True } + } else if new_head == state.food { + new_snake = snake_prepend(state.snake, new_head) + new_food = { x: (new_head.x + 3) % grid_size, y: (new_head.y + 3) % grid_size } + + { ..state, snake: new_snake, food: new_food } + } else { + grown = snake_prepend(state.snake, new_head) + moved = { first: grown.first, rest: List.drop_last(grown.rest, 1) } + + { ..state, snake: moved } + } + } +} -draw_game! : GameState => Result {} _ -draw_game! = |state| - clear_screen!({})? +hit_wall : Position -> Bool +hit_wall = |pos| + pos.x < 0 or pos.x >= grid_size or pos.y < 0 or pos.y >= grid_size - Stdout.line!("\nControls: W A S D to move, Q to quit\n\r")? +move_head : Position, Direction -> Position +move_head = |head, direction| + { x: head.x + direction.dx, y: head.y + direction.dy } - # \r to fix indentation because we're in raw mode - Stdout.line!("Score: ${Num.to_str(len(state.snake_lst) - init_snake_len)}\r")? +draw_game! : GameState => Try({}, _) +draw_game! = |state| { + clear_screen!()? - rendered_game_str = draw_game_pure(state) + Stdout.line!("\nControls: W A S D to move, Q to quit\n\r")? + Stdout.line!("Score: ${(snake_len(state.snake) - init_snake_len).to_str()}\r")? - Stdout.line!("${rendered_game_str}\r") + rendered_game_str = draw_game_pure(state) + Stdout.line!("${rendered_game_str}\r") +} draw_game_pure : GameState -> Str draw_game_pure = |state| - List.range({ start: At 0, end: Before grid_size }) - |> List.map( - |yy| - line = - List.range({ start: At 0, end: Before grid_size }) - |> List.map( - |xx| - pos = { x: xx, y: yy } - if contains(state.snake_lst, pos) then - if pos == state.snake_lst.first then - "O" # Snake head - else - "o" # Snake body - else if pos == state.food_pos then - "*" # food_pos - else - ".", # Empty space - ) - |> Str.join_with("") - - line, - ) - |> Str.join_with("\r\n") - -clear_screen! = |{}| - Stdout.write!("\u(001b)[2J\u(001b)[H") # ANSI escape codes to clear screen - -# NonEmptyList helpers - -contains : NonEmptyList, Position -> Bool -contains = |list, pos| - list.first == pos or List.contains(list.rest, pos) - -prepend : NonEmptyList, Position -> NonEmptyList -prepend = |list, pos| - { first: pos, rest: List.prepend(list.rest, list.first) } - -len : NonEmptyList -> U64 -len = |list| - 1 + List.len(list.rest) - -# Tests - -expect - grid_size == 20 # The tests below assume a grid size of 20 - -expect - initial_grid = draw_game_pure(initial_state) - expected_grid = - """ - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ........ooO.........\r - ....................\r - ....................\r - ....................\r - ....................\r - ...............*....\r - ....................\r - ....................\r - ....................\r - .................... - """ - - initial_grid == expected_grid - -# Test moving down -expect - new_state = update_game({ initial_state & direction: Down }) - new_grid = draw_game_pure(new_state) - - expected_grid = - """ - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - ....................\r - .........oo.........\r - ..........O.........\r - ....................\r - ....................\r - ....................\r - ...............*....\r - ....................\r - ....................\r - ....................\r - .................... - """ - - new_grid == expected_grid + 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({}, _) +clear_screen! = || Stdout.write!("\u(001b)[2J\u(001b)[H") + +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..2508ea03 100644 --- a/examples/time.roc +++ b/examples/time.roc @@ -1,23 +1,29 @@ +## Measure a sleep interval and format the current UTC time. app [main!] { pf: platform "../platform/main.roc" } +import pf.OsStr 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 +main! : List(OsStr) => Try({}, _) +main! = |_args| { -# Demo Utc and sleep functions + start : U128 + start = Utc.now!() -main! : List Arg => Result {} _ -main! = |_args| - start = Utc.now!({}) + Stdout.line!("Started at ${Utc.to_iso_8601(start)}")? - # 1000 ms = 1 second - Sleep.millis!(1000) + # 1000 ms = 1 second + Sleep.millis!(1000) - finish = Utc.now!({}) + finish : U128 + finish = Utc.now!() - duration = Num.to_str(Utc.delta_as_nanos(start, finish)) + duration_ms = Utc.delta_as_millis(finish, start) + duration_nanos = Utc.delta_as_nanos(finish, start) - Stdout.line!("Completed in ${duration} ns") + Stdout.line!("Completed in ${duration_ms.to_str()} ms (${duration_nanos.to_str()} ns)")? + + Ok({}) +} diff --git a/examples/tty.roc b/examples/tty.roc new file mode 100644 index 00000000..29d489e5 --- /dev/null +++ b/examples/tty.roc @@ -0,0 +1,23 @@ +## Enable terminal raw mode, read one key, and restore normal behavior. +app [main!] { pf: platform "../platform/main.roc" } + +import pf.OsStr +import pf.Stdout +import pf.Stdin +import pf.Tty + +main! : List(OsStr) => Try({}, _) +main! = |_args| { + Stdout.line!("Tty: enabling raw mode")? + Tty.enable_raw_mode!() + + Stdout.line!("Press one key...")? + input = Stdin.bytes!() + + Stdout.line!("Tty: disabling raw mode")? + Tty.disable_raw_mode!() + + bytes = input? + Stdout.line!("Read ${bytes.len().to_str()} byte(s).")? + Ok({}) +} diff --git a/examples/url.roc b/examples/url.roc new file mode 100644 index 00000000..60c88891 --- /dev/null +++ b/examples/url.roc @@ -0,0 +1,25 @@ +## Build a validated search URL while encoding path and query components. +app [main!] { pf: platform "../platform/main.roc" } + +import pf.OsStr +import pf.Stdout +import pf.Url + +main! : List(OsStr) => Try({}, _) +main! = |_args| { + base : Url.Url + base = "https://api.example.com" + search = base.append_path_segments(["v1", "search"]) + with_query = search.append_query_param("q", "roc lang") + with_page = with_query.append_query_param("page", "1") + url = with_page.with_fragment(Some("results")) ? |err| UrlBuildFailed(err) + + expect url.path() == "/v1/search" + expect url.query() == Some("q=roc+lang&page=1") + expect url.query_pairs() == [("q", "roc lang"), ("page", "1")] + + Stdout.line!("Request URL: ${url.to_str()}")? + Stdout.line!("Debug URL: ${Str.inspect(url)}")? + Stdout.line!("JSON URL: ${Json.to_str(url)}")? + 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..bd0f8567 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -1,238 +1,283 @@ -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 +import OsStr exposing [OsStr] -## 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 } - [ - StdoutContainsInvalidUtf8 { cmd_str : Str, err : [BadUtf8 { index : U64, problem : Str.Utf8Problem }] }, - NonZeroExitCode { command : Str, exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str }, - FailedToGetExitCode { command : Str, err : IOErr }, - ] -exec_output! = |@Cmd(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, - }, - ) - -## 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) }) +## Build and run child processes with native-safe programs, arguments, and +## environment values. +Cmd :: { + args : List(OsStr), + clear_envs : Bool, + envs : List((OsStr, OsStr)), + program : OsStr, +}.{ + + ## 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! : OsStr, List(OsStr) => Try({}, [ExecFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode({ command : Str, err : IOErr }), ..]) + exec! = |program, arguments| { + command = "${OsStr.display(program)} ${Str.join_with(arguments.map(OsStr.display), " ")}" + + exit_code = new(program) + .args(arguments) + .exec_exit_code!()? + + if exit_code == 0 { + Ok({}) + } else { + 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| { + command = to_str(cmd) + exit_code = exec_exit_code!(cmd)? + + if exit_code == 0 { + Ok({}) + } else { + Err(ExecCmdFailed({ command, 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| { + cmd_str = to_str(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, err }))? + + stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) + + Ok({ stdout_utf8, stderr_utf8_lossy }) + } + + Err(NonZeroExitCode({ 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: cmd_str, exit_code, stdout_utf8_lossy, stderr_utf8_lossy })) + } + + Err(FailedToGetExitCode(err)) => Err(FailedToGetExitCode({ command: cmd_str, 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) }, [NonZeroExitCodeB({ exit_code : I32, stdout_bytes : List(U8), stderr_bytes : List(U8) }), FailedToGetExitCodeB(IOErr), ..]) + exec_output_bytes! = |cmd| { + exec_try = Host.cmd_exec_output!(to_host_cmd(cmd)) + + match exec_try { + Ok({ stderr_bytes, stdout_bytes }) => + Ok({ stdout_bytes, stderr_bytes }) + + Err(NonZeroExitCode({ exit_code, stderr_bytes, stdout_bytes })) => { + Err(NonZeroExitCodeB({ exit_code, stdout_bytes, stderr_bytes })) + } + + Err(FailedToGetExitCode(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| { + command = to_str(cmd) + + match Host.cmd_exec_exit_code!(to_host_cmd(cmd)) { + Ok(num) => Ok(num) + Err(io_err) => Err(FailedToGetExitCode({ command, 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 : OsStr -> Cmd + new = |program| { + args: [], + clear_envs: Bool.False, + envs: [], + program, + } + + ## Create a new command from a Roc string. + new_str : Str -> Cmd + new_str = |program| new(OsStr.from_str(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, OsStr -> Cmd + arg = |cmd, a| { + ..cmd, + args: cmd.args.append(a), + } + + ## Add a single string argument to the command. + arg_str : Cmd, Str -> Cmd + arg_str = |cmd, a| arg(cmd, OsStr.from_str(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(OsStr) -> Cmd + args = |cmd, new_args| { + ..cmd, + args: cmd.args.concat(new_args), + } + + ## Add multiple string arguments to the command. + args_str : Cmd, List(Str) -> Cmd + args_str = |cmd, new_args| args(cmd, new_args.map(OsStr.from_str)) + + ## 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, OsStr, OsStr -> Cmd + env = |cmd, key, value| { + { ..cmd, envs: cmd.envs.append((key, value)) } + } + + ## Add a single string environment variable to the command. + env_str : Cmd, Str, Str -> Cmd + env_str = |cmd, key, value| env(cmd, OsStr.from_str(key), OsStr.from_str(value)) + + ## Add multiple environment variables to the command. + ## + ## ```roc + ## cmd = Cmd.new("env").envs([("FOO", "bar"), ("BAZ", "qux")]) + ## ``` + envs : Cmd, List((OsStr, OsStr)) -> Cmd + envs = |cmd, pairs| { ..cmd, envs: cmd.envs.concat(pairs) } + + ## Add multiple string environment variables to the command. + envs_str : Cmd, List((Str, Str)) -> Cmd + envs_str = |cmd, pairs| { + arg_pairs = pairs.map(|(key, value)| (OsStr.from_str(key), OsStr.from_str(value))) + envs(cmd, arg_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 } + + ## Render a command configuration as a stable, escaped string. + to_str : Cmd -> Str + to_str = |cmd| + "Cmd({ program: ${Str.inspect(cmd.program)}, args: ${Str.inspect(cmd.args)}, envs: ${Str.inspect(cmd.envs)}, clear_envs: ${Str.inspect(cmd.clear_envs)} })" + + ## Customize command output for `Str.inspect`. + to_inspect : Cmd -> Str + to_inspect = |cmd| to_str(cmd) +} + +flatten_arg_pairs : List((OsStr, OsStr)), List(OsStr), U64 -> List(OsStr) +flatten_arg_pairs = |pairs, acc, idx| { + if idx >= pairs.len() { + acc + } else { + match pairs.get(idx) { + Ok(pair) => + flatten_arg_pairs(pairs, acc.append(pair.0).append(pair.1), idx + 1) + Err(_) => + acc + } + } +} + +to_host_cmd : Cmd -> Host.Cmd +to_host_cmd = |cmd| { + args: cmd.args.map(OsStr.to_raw), + clear_envs: cmd.clear_envs, + envs: flatten_arg_pairs(cmd.envs, [], 0).map(OsStr.to_raw), + program: OsStr.to_raw(cmd.program), +} + +## Inspection is escaped and includes the full immutable command configuration. +expect { + cmd = Cmd.new_str("echo\nnext") + .arg_str("hello world") + .env_str("NAME", "Roc") + .clear_envs() + + Str.inspect(cmd) == "Cmd({ program: OsStr.utf8(\"echo\\nnext\"), args: [OsStr.utf8(\"hello world\")], envs: [(OsStr.utf8(\"NAME\"), OsStr.utf8(\"Roc\"))], clear_envs: True })" +} diff --git a/platform/Dir.roc b/platform/Dir.roc deleted file mode 100644 index a359fe06..00000000 --- a/platform/Dir.roc +++ /dev/null @@ -1,72 +0,0 @@ -module [ - IOErr, - list!, - create!, - create_all!, - delete_empty!, - delete_all!, -] - -import Path exposing [Path] -import InternalIOErr - -## 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 - -## 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 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)) - -## 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)) diff --git a/platform/Env.roc b/platform/Env.roc index 1253bc82..b0f9f0e5 100644 --- a/platform/Env.roc +++ b/platform/Env.roc @@ -1,171 +1,85 @@ -module [ - cwd!, - dict!, - var!, - decode!, - exe_path!, - set_cwd!, - platform!, - temp_dir!, -] - -import Path exposing [Path] -import InternalPath -import EnvDecoding import Host +import IOErr exposing [IOErr] +import OsStr exposing [OsStr] +import Path -## 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. +## Read and modify the process environment without losing native OS strings. ## -temp_dir! : {} => Path -temp_dir! = |{}| - Host.temp_dir!({}) - |> InternalPath.from_os_bytes +## Variable names and values use [`OsStr`](OsStr) because Unix environment data +## is not required to be UTF-8. Use [`var_str!`](#var_str!) when an application +## specifically requires text. Paths use basic-cli's byte-preserving `Path` type. +Env :: [].{ + + ## Report the architecture and operating system for which the host was built. + platform! : () => { + arch : [X86, X64, ARM, AARCH64, OTHER(Str)], + os : [LINUX, MACOS, WINDOWS, OTHER(Str)], + } + platform! = || Host.env_platform!() + + ## Return all environment variables as native name/value pairs. + ## + ## Native non-Unicode values are preserved. Iteration order is unspecified + ## and may differ between calls or operating systems. + dict! : () => List((OsStr, OsStr)) + dict! = || + List.map(Host.env_dict!(), |(name, value)| (OsStr.from_raw(name), OsStr.from_raw(value))) + + ## Reads the given environment variable. + ## + ## Returns `Err(VarNotFound(name))` if the variable is not set. + var! : OsStr => Try(OsStr, [VarNotFound(OsStr), EnvErr(IOErr), ..]) + var! = |name| + match Host.env_var!(OsStr.to_raw(name)) { + Ok(raw) => Ok(OsStr.from_raw(raw)) + Err(VarNotFound(raw_name)) => Err(VarNotFound(OsStr.from_raw(raw_name))) + Err(EnvErr(err)) => Err(EnvErr(err)) + } + + ## Reads the given environment variable as a string if its native value is valid text. + var_str! : OsStr => Try(Str, [VarNotFound(OsStr), EnvErr(IOErr), InvalidStr(U64), ..]) + var_str! = |name| + match var!(name) { + Ok(value) => + match OsStr.to_str_try(value) { + Ok(str) => Ok(str) + Err(InvalidStr(index)) => Err(InvalidStr(index)) + } + Err(VarNotFound(raw_name)) => Err(VarNotFound(raw_name)) + Err(EnvErr(err)) => Err(EnvErr(err)) + } + + ## 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(Path.Path, [CwdUnavailable, ..]) + cwd! = || + match Host.env_cwd!() { + Ok(raw) => Ok(Path.from_raw(raw)) + Err(CwdUnavailable) => Err(CwdUnavailable) + } + + ## Change the process current working directory. + ## + ## Returns `Err(InvalidCwd(err))` when the path cannot be used as a working + ## directory. The process-wide change remains in effect until changed again. + set_cwd! : Path.Path => Try({}, [InvalidCwd(IOErr), ..]) + set_cwd! = |path| + Host.env_set_cwd!(Path.to_raw(path)) + .map_err(|err| InvalidCwd(err)) + + ## Gets the path to the currently-running executable. + ## + ## Returns `Err(ExePathUnavailable)` if the path cannot be determined. + exe_path! : () => Try(Path.Path, [ExePathUnavailable, ..]) + exe_path! = || + match Host.env_exe_path!() { + Ok(raw) => Ok(Path.from_raw(raw)) + Err(ExePathUnavailable) => Err(ExePathUnavailable) + } + + ## Gets the default directory for temporary files. + temp_dir! : () => Path.Path + temp_dir! = || Path.from_raw(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..a50b5147 100644 --- a/platform/File.roc +++ b/platform/File.roc @@ -1,355 +1,44 @@ -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 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))) +import Path + +## Open files for incremental, buffered reading. +## +## Whole-file operations and filesystem metadata are available on [`Path`](Path). +File :: [].{ + + ## Represents a buffered file reader. + ## + ## The file is automatically closed when the last reference to the reader is + ## dropped. It wraps an opaque host-side `BufReader` handle. + Reader :: { host : Host.FileReader }.{ + + ## Render the reader without exposing its host handle. + to_inspect : Reader -> Str + to_inspect = |_| "File.Reader()" + + ## Read bytes up to and including the next newline from this buffered reader. + ## + ## Returns an empty list at EOF. + read_line! : Reader => Try(List(U8), _) + read_line! = |reader| + Host.file_read_line!(reader.host) + .map_err(|FileErr(err)| FileErr(err)) + } + + ## Open a file for buffered reading using the default buffer capacity. + ## + ## ```roc + ## reader = File.open_reader!("LICENSE")? + ## line = reader.read_line!()? + ## ``` + open_reader! = |path| + Host.file_open_reader!(Path.to_raw(path), 0) + .map_ok(|reader| Reader.{ host: reader }) + .map_err(|FileErr(err)| FileErr(err)) + + ## Open a file for buffered reading using a specific buffer capacity. + open_reader_with_capacity! = |path, capacity| + Host.file_open_reader!(Path.to_raw(path), capacity) + .map_ok(|reader| Reader.{ host: reader }) + .map_err(|FileErr(err)| FileErr(err)) +} diff --git a/platform/Host.roc b/platform/Host.roc index 4e83fdc7..286b8b99 100644 --- a/platform/Host.roc +++ b/platform/Host.roc @@ -1,153 +1,125 @@ -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 + +## Declare the hosted effects and ABI-safe data exchanged with the native host. +Host :: [].{ + NativeOsStr : [Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))] + NativePath : [Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))] + + Cmd : { + args : List(NativeOsStr), + clear_envs : Bool, + envs : List(NativeOsStr), + program : NativeOsStr, + } + + 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, [NonZeroExitCode(CmdOutputFailure), FailedToGetExitCode(IOErr)]) + + dir_create! : NativePath => Try({}, [DirErr(IOErr)]) + dir_create_all! : NativePath => Try({}, [DirErr(IOErr)]) + dir_delete_empty! : NativePath => Try({}, [DirErr(IOErr)]) + dir_delete_all! : NativePath => Try({}, [DirErr(IOErr)]) + dir_list! : NativePath => Try(List(NativePath), [DirErr(IOErr)]) + + env_var! : NativeOsStr => Try(NativeOsStr, [VarNotFound(NativeOsStr), EnvErr(IOErr)]) + env_cwd! : () => Try(NativePath, [CwdUnavailable]) + env_exe_path! : () => Try(NativePath, [ExePathUnavailable]) + env_temp_dir! : () => NativePath + + file_read_bytes! : NativePath => Try(List(U8), [FileErr(IOErr)]) + file_write_bytes! : NativePath, List(U8) => Try({}, [FileErr(IOErr)]) + file_read_utf8! : NativePath => Try(Str, [FileErr(IOErr)]) + file_write_utf8! : NativePath, Str => Try({}, [FileErr(IOErr)]) + file_open_reader! : NativePath, U64 => Try(FileReader, [FileErr(IOErr)]) + file_read_line! : FileReader => Try(List(U8), [FileErr(IOErr)]) + file_delete! : NativePath => Try({}, [FileErr(IOErr)]) + file_size_in_bytes! : NativePath => Try(U64, [FileErr(IOErr)]) + file_is_executable! : NativePath => Try(Bool, [FileErr(IOErr)]) + file_is_readable! : NativePath => Try(Bool, [FileErr(IOErr)]) + file_is_writable! : NativePath => Try(Bool, [FileErr(IOErr)]) + file_time_accessed! : NativePath => Try(U128, [FileErr(IOErr)]) + file_time_modified! : NativePath => Try(U128, [FileErr(IOErr)]) + file_time_created! : NativePath => 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! : NativePath => Try(PathType, IOErr) + + random_seed_u64! : () => Try(U64, [RandomErr(IOErr)]) + random_seed_u32! : () => Try(U32, [RandomErr(IOErr)]) + + sleep_millis! : U64 => {} + + sqlite_prepare! : NativePath, 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! : () => {} + + # TODO(https://github.com/roc-lang/roc/issues/10163): revert to a bare U128 + # return once the compiler emits the clang/Rust u128 return convention on + # x86_64-windows; bare U128 returns are currently misread there, while + # Try-wrapped results cross the boundary correctly on every target. + utc_now! : () => Try(U128, [ClockBeforeEpoch]) + + # New hosted functions are kept at the end to avoid renumbering existing + # generated glue more than necessary. + file_hard_link! : NativePath, NativePath => Try({}, [FileErr(IOErr)]) + file_rename! : NativePath, NativePath => Try({}, [FileErr(IOErr)]) + env_platform! : () => { + arch : [X86, X64, ARM, AARCH64, OTHER(Str)], + os : [LINUX, MACOS, WINDOWS, OTHER(Str)], + } + env_dict! : () => List((NativeOsStr, NativeOsStr)) + env_set_cwd! : NativePath => Try({}, IOErr) +} diff --git a/platform/Http.roc b/platform/Http.roc index 433125f2..76c0b78a 100644 --- a/platform/Http.roc +++ b/platform/Http.roc @@ -1,146 +1,103 @@ -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 Url +import http.Request +import http.Response + +## Send requests using the shared +## [`roc-lang/http`](https://github.com/roc-lang/http) `Request` and `Response` +## types. This module supplies effects and small JSON/UTF-8 conveniences while +## leaving pure request and response construction to that package. +Http :: [].{ + + ## Errors raised by the host while sending a request, before a real HTTP + ## response is available. + TransportErr : InternalHttp.TransportErr + + ## Validate and send an HTTP request. + ## + ## The request URI must be an absolute HTTP or HTTPS URL accepted by Url. + ## Invalid URLs return InvalidUrl before any host effect occurs. Fragments + ## are removed because they are client-side identifiers and are not sent. + ## + ## ```roc + ## request = Request.from_method(GET).with_uri("https://www.roc-lang.org") + ## response = Http.send!(request)? + ## ``` + send! : Request => Try(Response, [InvalidUrl(Url.ParseErr), HttpErr(TransportErr), ..]) + send! = |request| { + url = Url.parse(Request.uri(request)) ? InvalidUrl + canonical_url = Url.without_fragment(url) + canonical_request = request.with_uri(Url.to_str(canonical_url)) + host_response = Host.http_send_request!(InternalHttp.to_host_request(canonical_request)) ? HttpErr + + Ok(InternalHttp.from_host_response(host_response)) + } + + ## Encode a value as JSON and set it as the request body. + ## + ## This uses Roc's builtin JSON encoder, so the value's type determines the + ## encoder through static dispatch. + with_json_body : Request, _ => Try(Request, [JsonErr(_), ..]) + with_json_body = |request, value| { + body = Json.to_str_try(value) ? JsonErr + + Ok( + request + .add_header("Content-Type", "application/json") + .with_body(Str.to_utf8(body)), + ) + } + + ## Encode a value as JSON, attach it to the request body, and send it. + send_json! : Request, _ => Try(Response, [JsonErr(_), InvalidUrl(Url.ParseErr), HttpErr(TransportErr), ..]) + send_json! = |request, value| { + json_request = with_json_body(request, value)? + + send!(json_request) + } + + ## Perform an HTTP GET and decode the response body as a UTF-8 `Str`. + ## + ## The argument is a validated Url. Quoted literals work through + ## Url.from_quote; dynamic strings should be passed through Url.parse. + ## + ## ```roc + ## hello_str = Http.get_utf8!("http://localhost:8000")? + ## ``` + get_utf8! : Url.Url => Try(Str, [BadBody(Str), InvalidUrl(Url.ParseErr), HttpErr(TransportErr), ..]) + get_utf8! = |url| { + response = send!(Request.from_method(GET).with_uri(Url.to_str(url)))? + body = Str.from_utf8(Response.body(response)) ? |_| BadBody("get_utf8!: response body was not valid UTF-8") + + Ok(body) + } + + ## Decode a response body as JSON. + ## + ## This uses Roc's builtin JSON parser, so the expected result type + ## determines the parser through static dispatch. + decode_json_response : Response => Try(_, [BadBody(Str), JsonErr(_), ..]) + decode_json_response = |response| { + body = Str.from_utf8(Response.body(response)) ? |_| BadBody("decode_json_response: response body was not valid UTF-8") + decoded = Json.parse(body) ? JsonErr + + Ok(decoded) + } + + ## Perform an HTTP GET and decode the response body as JSON. + ## + ## The argument is a validated Url. JSON parser failures are returned as + ## JsonErr(_). + ## + ## ```roc + ## payload : Try({ foo : Str }, _) + ## payload = Http.get!("http://localhost:8000") + ## ``` + get! : Url.Url => Try(_, [BadBody(Str), InvalidUrl(Url.ParseErr), HttpErr(TransportErr), JsonErr(_), ..]) + get! = |url| { + response = send!(Request.from_method(GET).with_uri(Url.to_str(url)))? + + decode_json_response(response) + } } - -## 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/IOErr.roc b/platform/IOErr.roc new file mode 100644 index 00000000..663d61fc --- /dev/null +++ b/platform/IOErr.roc @@ -0,0 +1,53 @@ +## Represents an I/O error that can occur during platform operations. +## +## **NotFound** - An entity was not found, often a file. +## +## **PermissionDenied** - The operation lacked the necessary privileges to complete. +## +## **BrokenPipe** - The operation failed because a pipe was closed. +## +## **AlreadyExists** - An entity already exists, often a file. +## +## **Interrupted** - This operation was interrupted. Interrupted operations can typically be retried. +## +## **IsADirectory** - A filesystem operation expected a non-directory path. +## +## **NotADirectory** - A filesystem operation expected a directory path. +## +## **Unsupported** - This operation is unsupported on this platform. This means that the operation can never succeed. +## +## **OutOfMemory** - An operation could not be completed, because it failed to allocate enough memory. +## +## **Other** - A custom error that does not fall under any other I/O error kind. +IOErr := [ + AlreadyExists, + BrokenPipe, + Interrupted, + IsADirectory, + NotFound, + NotADirectory, + Other(Str), + OutOfMemory, + PermissionDenied, + Unsupported, +].{ + + ## Convert an I/O error to a concise human-readable message. + to_str : IOErr -> Str + to_str = |err| + match err { + AlreadyExists => "entity already exists" + BrokenPipe => "pipe is closed" + Interrupted => "operation was interrupted" + IsADirectory => "expected a non-directory path, but found a directory" + NotFound => "entity was not found" + NotADirectory => "expected a directory, but found a non-directory path" + Other(message) => message + OutOfMemory => "operation could not allocate enough memory" + PermissionDenied => "permission denied" + Unsupported => "operation is unsupported" + } +} + +expect IOErr.to_str(NotFound) == "entity was not found" +expect IOErr.to_str(Other("device unavailable")) == "device unavailable" 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 index 4e639927..f15cdd44 100644 --- a/platform/InternalDateTime.roc +++ b/platform/InternalDateTime.roc @@ -1,221 +1,128 @@ -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" +## Convert Unix timestamps into UTC calendar dates for public time formatting. +InternalDateTime :: [].{ + DateTime : { + day : U128, + hours : U128, + minutes : U128, + month : U128, + seconds : U128, + year : U128, + } + + to_iso_8601 : DateTime -> Str + to_iso_8601 = |{ year, month, day, hours, minutes, seconds }| { + year_str = year_with_padded_zeros(year) + month_str = two_digits(month) + day_str = two_digits(day) + hour_str = two_digits(hours) + minute_str = two_digits(minutes) + seconds_str = two_digits(seconds) + + "${year_str}-${month_str}-${day_str}T${hour_str}:${minute_str}:${seconds_str}Z" + } + + epoch_millis_to_datetime : U128 -> DateTime + epoch_millis_to_datetime = |millis| { + seconds = millis // 1_000 + minutes = seconds // 60 + hours = minutes // 60 + + normalize_date({ + year: 1970, + month: 1, + day: 1 + hours // 24, + hours: hours % 24, + minutes: minutes % 60, + seconds: seconds % 60, + }) + } + + year_with_padded_zeros : U128 -> Str + year_with_padded_zeros = |year| { + year_str = year.to_str() + + if year < 10 { + "000${year_str}" + } else if year < 100 { + "00${year_str}" + } else if year < 1000 { + "0${year_str}" + } else { + year_str + } + } + + two_digits : U128 -> Str + two_digits = |value| { + value_str = value.to_str() + + if value < 10 { + "0${value_str}" + } else { + value_str + } + } + + normalize_date : DateTime -> DateTime + normalize_date = |current| { + days_this_month = days_in_month(current.year, current.month) + + if current.day > days_this_month { + normalize_date({ + ..current, + year: if current.month == 12 { + current.year + 1 + } else { + current.year + }, + month: if current.month == 12 { + 1 + } else { + current.month + 1 + }, + day: current.day - days_this_month, + }) + } else { + current + } + } + + days_in_month : U128, U128 -> U128 + days_in_month = |year, month| + match month { + 1 => 31 + 2 => if is_leap_year(year) { + 29 + } else { + 28 + } + 3 => 31 + 4 => 30 + 5 => 31 + 6 => 30 + 7 => 31 + 8 => 31 + 9 => 30 + 10 => 31 + 11 => 30 + 12 => 31 + _ => 0 + } + + is_leap_year : U128 -> Bool + is_leap_year = |year| { + divisible_by_4 = year % 4 == 0 + divisible_by_100 = year % 100 == 0 + divisible_by_400 = year % 400 == 0 + + if divisible_by_4 { + if divisible_by_100 { + divisible_by_400 + } else { + True + } + } else { + False + } + } +} diff --git a/platform/InternalHttp.roc b/platform/InternalHttp.roc index 6bf0287b..9bde5620 100644 --- a/platform/InternalHttp.roc +++ b/platform/InternalHttp.roc @@ -1,139 +1,89 @@ -# 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 + +## Define host-ABI HTTP types and convert them to the shared HTTP package types. +## These records map directly to the generated Rust glue types. +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 -> 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 + 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/InternalIOErr.roc b/platform/InternalIOErr.roc deleted file mode 100644 index 033a0da6..00000000 --- a/platform/InternalIOErr.roc +++ /dev/null @@ -1,58 +0,0 @@ -module [ - IOErr, - IOErrFromHost, - handle_err, -] - -## **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, -] - -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/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..b792d597 100644 --- a/platform/InternalSqlite.roc +++ b/platform/InternalSqlite.roc @@ -1,29 +1,26 @@ -module [ - SqliteError, - SqliteValue, - SqliteState, - SqliteBindings, -] +## Define the host-ABI types shared by SQLite effects and the public module. +## These records map directly to the generated Rust glue types. +InternalSqlite :: [].{ + SqliteError : { + code : I64, + message : Str, + } -SqliteError : { - code : I64, - message : Str, -} - -SqliteValue : [ - Null, - Real F64, - Integer I64, - String Str, - Bytes (List U8), -] + SqliteValue : [ + Null, + Real(F64), + Integer(I64), + String(Str), + Bytes(List(U8)), + ] -SqliteState : [ - Row, - Done, -] + SqliteState : [ + Row, + Done, + ] -SqliteBindings : { - name : Str, - value : SqliteValue, + SqliteBindings : { + name : Str, + value : SqliteValue, + } } diff --git a/platform/Locale.roc b/platform/Locale.roc index 9c32fe72..b41ed0fa 100644 --- a/platform/Locale.roc +++ b/platform/Locale.roc @@ -1,20 +1,324 @@ -module [ - get!, - all!, -] - import Host -## Returns the most preferred locale for the system or application, or `NotAvailable` if the locale could not be obtained. +## A locale represented as a pragmatically validated BCP 47 language tag. ## -## 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 spelling is preserved for display, while equality and hashing are +## ASCII case-insensitive as required for language tags. +Locale :: { raw : Str }.{ -## 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! + ## A reason a locale string could not be parsed. + ParseErr : [ + Empty, + EmptySubtag, + InvalidCharacter, + InvalidLanguage, + MissingExtensionValue, + SubtagTooLong, + ] + + ## Parse a dynamic string as a locale. + ## + ## This catches common language-tag mistakes without consulting the IANA + ## registry or implementing every structural rule in BCP 47. + parse : Str -> Try(Locale, ParseErr) + parse = |input| + match validate(input) { + Ok({}) => Ok(Locale.{ raw: input }) + Err(err) => Err(err) + } + + ## Parse and validate a quoted locale literal. + ## + ## Roc calls this automatically when a quoted literal is expected to have + ## type Locale. + from_quote : Str -> Try(Locale, [BadQuotedBytes(Str)]) + from_quote = |input| + match parse(input) { + Ok(locale) => Ok(locale) + Err(err) => Err(BadQuotedBytes(parse_err_to_str(err))) + } + + ## Parse a locale from a string value supplied by a generic encoding. + parser_for : encoding -> (state -> Try({ value : Locale, rest : state }, err)) + where [ + encoding.parse_str : encoding, state -> Try({ value : Str, rest : state }, err), + encoding.invalid_value : encoding, state -> err, + ] + parser_for = |encoding| { + Encoding : encoding + + |state| { + parsed = Encoding.parse_str(encoding, state)? + + match parse(parsed.value) { + Ok(locale) => Ok({ value: locale, rest: parsed.rest }) + Err(_) => Err(Encoding.invalid_value(encoding, state)) + } + } + } + + ## Encode a locale using its original spelling through a generic encoding. + encoder_for : encoding -> (Locale, state -> Try(state, err)) + where [ + encoding.encode_str : Str, state -> Try(state, err), + ] + encoder_for = |_encoding| { + Encoding : encoding + + |locale, state| Encoding.encode_str(to_str(locale), state) + } + + ## Return the locale using its original spelling. + to_str : Locale -> Str + to_str = |locale| locale.raw + + ## Render a locale for debugging and test failures. + to_inspect : Locale -> Str + to_inspect = |locale| "Locale(${Json.to_str(locale.raw)})" + + ## Compare locale tags without ASCII case distinctions. + is_eq : Locale, Locale -> Bool + is_eq = |left, right| ascii_lower(left.raw) == ascii_lower(right.raw) + + ## Hash locale tags without ASCII case distinctions. + to_hash : Locale, Hasher -> Hasher + to_hash = |locale, hasher| Str.to_hash(ascii_lower(locale.raw), hasher) + + ## Returns the most preferred locale for the system or application. + ## + ## Host locale strings are trusted because the platform host is responsible + ## for returning BCP 47 language tags. + ## + ## Returns `Err(NotAvailable)` if the locale cannot be determined. + get! : () => Try(Locale, [NotAvailable, ..]) + get! = || { + raw = widen_locale_err(Host.locale_get!())? + Ok(Locale.{ raw }) + } + + ## Returns the preferred locales for the system or application. + ## + ## Host locale strings are trusted because the platform host is responsible + ## for returning BCP 47 language tags. + all! : () => List(Locale) + all! = || Host.locale_all!().map(|raw| Locale.{ raw }) +} + +validate : Str -> Try({}, Locale.ParseErr) +validate = |input| + if Str.is_empty(input) { + Err(Empty) + } else { + subtags = Str.split_on(input, "-") + + if List.any(subtags, |subtag| Str.is_empty(subtag)) { + Err(EmptySubtag) + } else if List.any(subtags, |subtag| has_invalid_character(subtag)) { + Err(InvalidCharacter) + } else if List.any(subtags, |subtag| List.len(Str.to_utf8(subtag)) > 8) { + Err(SubtagTooLong) + } else { + validate_structure(subtags) + } + } + +validate_structure : List(Str) -> Try({}, Locale.ParseErr) +validate_structure = |subtags| + match subtags { + [] => Err(Empty) + [language, .. as rest] => { + language_bytes = Str.to_utf8(language) + language_len = List.len(language_bytes) + is_special = language_len == 1 and (ascii_byte_eq(language_bytes, 'x') or ascii_byte_eq(language_bytes, 'i')) + + if is_special { + if List.is_empty(rest) { + Err(MissingExtensionValue) + } else if ascii_byte_eq(language_bytes, 'x') { + Ok({}) + } else { + validate_extensions(rest) + } + } else if language_len < 2 or Bool.not(List.all(language_bytes, is_ascii_alpha)) { + Err(InvalidLanguage) + } else { + validate_extensions(rest) + } + } + } + +validate_extensions : List(Str) -> Try({}, Locale.ParseErr) +validate_extensions = |subtags| + match subtags { + [] => Ok({}) + [subtag, .. as rest] => { + bytes = Str.to_utf8(subtag) + + if List.len(bytes) == 1 { + if List.is_empty(rest) { + Err(MissingExtensionValue) + } else if ascii_byte_eq(bytes, 'x') { + Ok({}) + } else { + validate_extensions(rest) + } + } else { + validate_extensions(rest) + } + } + } + +has_invalid_character : Str -> Bool +has_invalid_character = |subtag| Bool.not(List.all(Str.to_utf8(subtag), is_ascii_alphanumeric)) + +is_ascii_alpha : U8 -> Bool +is_ascii_alpha = |byte| (byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122) + +is_ascii_digit : U8 -> Bool +is_ascii_digit = |byte| byte >= 48 and byte <= 57 + +is_ascii_alphanumeric : U8 -> Bool +is_ascii_alphanumeric = |byte| is_ascii_alpha(byte) or is_ascii_digit(byte) + +ascii_byte_eq : List(U8), U8 -> Bool +ascii_byte_eq = |bytes, expected| + match bytes { + [byte] => byte == expected or byte == expected - 32 + _ => False + } + +ascii_lower : Str -> Str +ascii_lower = |input| + Str.from_utf8_lossy( + Str.to_utf8(input).map( + |byte| + if byte >= 65 and byte <= 90 { + byte + 32 + } else { + byte + }, + ), + ) + +parse_err_to_str : Locale.ParseErr -> Str +parse_err_to_str = |err| + match err { + Empty => "Locale must not be empty" + EmptySubtag => "Locale subtags must not be empty" + InvalidCharacter => "Locale must contain only ASCII letters, digits, and hyphens" + InvalidLanguage => "Locale must start with a 2-8 letter language subtag" + MissingExtensionValue => "Locale extension or private-use marker must be followed by a subtag" + SubtagTooLong => "Locale subtags must be at most 8 characters" + } + +widen_locale_err : Try(a, [NotAvailable]) -> Try(a, [NotAvailable, ..]) +widen_locale_err = |result| + match result { + Ok(value) => Ok(value) + Err(NotAvailable) => Err(NotAvailable) + } + +expect + match Locale.parse("en-US") { + Ok(locale) => Locale.to_str(locale) == "en-US" + Err(_) => False + } + +expect + match Locale.parse("zh-Hant-TW") { + Ok(locale) => Locale.to_str(locale) == "zh-Hant-TW" + Err(_) => False + } + +expect Locale.parse("de-CH-1901").is_ok() + +expect Locale.parse("en-u-ca-gregory").is_ok() + +expect Locale.parse("x-private").is_ok() + +expect Locale.parse("x-a").is_ok() + +expect Locale.parse("en-x-a").is_ok() + +expect Locale.parse("i-default").is_ok() + +expect Locale.parse("") == Err(Empty) + +expect Locale.parse("en--US") == Err(EmptySubtag) + +expect Locale.parse("-en") == Err(EmptySubtag) + +expect Locale.parse("en-") == Err(EmptySubtag) + +expect Locale.parse("en_US") == Err(InvalidCharacter) + +expect Locale.parse("en-café") == Err(InvalidCharacter) + +expect Locale.parse("1n-US") == Err(InvalidLanguage) + +expect Locale.parse("a-DE") == Err(InvalidLanguage) + +expect Locale.parse("en-toolongtag") == Err(SubtagTooLong) + +expect Locale.parse("en-u") == Err(MissingExtensionValue) + +expect { + locale : Locale + locale = "en-US" + Locale.to_str(locale) == "en-US" +} + +expect + match (Locale.parse("en-US"), Locale.parse("EN-us")) { + (Ok(left), Ok(right)) => left == right and Locale.to_str(right) == "EN-us" + _ => False + } + +expect + match (Locale.parse("en-US"), Locale.parse("fr-FR")) { + (Ok(left), Ok(right)) => left != right + _ => False + } + +expect + match (Locale.parse("en-US"), Locale.parse("EN-us")) { + (Ok(stored), Ok(lookup)) => Dict.single(stored, "found").get(lookup) == Ok("found") + _ => False + } + +expect + match Locale.from_quote("en_US") { + Err(BadQuotedBytes(message)) => Str.contains(message, "ASCII letters, digits, and hyphens") + Ok(_) => False + } + +## Inspection preserves spelling and identifies the nominal type. +expect + match Locale.parse("EN-us") { + Ok(locale) => Str.inspect(locale) == "Locale(\"EN-us\")" + Err(_) => False + } + +## Generic encoders preserve the locale's original spelling. +expect { + locale : Locale + locale = "en-US" + Json.to_str(locale) == "\"en-US\"" +} + +## Generic parsers validate encoded locale strings. +expect { + decoded : Try(Locale, Json.ParseErr) + decoded = Json.parse("\"zh-Hant-TW\"") + + match decoded { + Ok(locale) => Locale.to_str(locale) == "zh-Hant-TW" + Err(_) => False + } +} + +expect { + decoded : Try(Locale, Json.ParseErr) + decoded = Json.parse("\"en_US\"") + decoded == Err(Json.invalid_json) +} diff --git a/platform/OsStr.roc b/platform/OsStr.roc new file mode 100644 index 00000000..ccb79135 --- /dev/null +++ b/platform/OsStr.roc @@ -0,0 +1,247 @@ +## Represent operating-system strings without losing non-Unicode data. +## +## Native Unix bytes and Windows UTF-16 units roundtrip through host effects; +## use [`display`](#display) only when a lossy representation is acceptable. +OsStr := [ + Utf8(Str), + UnixBytes(List(U8)), + WindowsU16s(List(U16)), +].{ + + ## Create an OS string from UTF-8 text. + ## The host lowers this text to the active OS representation. + from_str : Str -> OsStr + from_str = |str| Utf8(str) + + ## Create a UTF-8 text OS string. + utf8 : Str -> OsStr + utf8 = |str| Utf8(str) + + ## Create a Unix OS string from a Roc string by storing its UTF-8 bytes. + unix : Str -> OsStr + unix = |str| UnixBytes(Str.to_utf8(str)) + + ## Create a Unix OS string from raw bytes without validating UTF-8. + unix_bytes : List(U8) -> OsStr + unix_bytes = |bytes| UnixBytes(bytes) + + ## Create a Windows OS string from a Roc string by storing its UTF-16 code units. + windows : Str -> OsStr + windows = |str| from_raw(WindowsU16s(str_to_utf16(str))) + + ## Create a Windows OS string from raw UTF-16 code units. + windows_u16s : List(U16) -> OsStr + windows_u16s = |u16s| WindowsU16s(u16s) + + ## Build an OS string from a quoted string literal. + from_quote : Str -> Try(OsStr, [BadQuotedBytes(Str)]) + from_quote = |str| Ok(Utf8(str)) + + ## Build a UTF-8 OS string from an interpolated string literal. + from_interpolation : Str, Iter((Str, Str)) -> OsStr + from_interpolation = |first, rest| + Utf8(rest.fold(first, |acc, (interpolated, segment)| acc.concat(interpolated).concat(segment))) + + ## TODO: Restore generic parser_for and encoder_for helpers when the compiler + ## no longer treats auto-derived `_` declarations in platforms as hosted: + ## https://github.com/roc-lang/roc/issues/10162 + + ## Convert an OS string to a string if its raw representation is valid text. + to_str_try : OsStr -> Try(Str, [InvalidStr(U64)]) + to_str_try = |os_str| + match to_raw(os_str) { + Utf8(str) => Ok(str) + UnixBytes(bytes) => + match Str.from_utf8(bytes) { + Ok(str) => Ok(str) + Err(BadUtf8({ index, problem: _ })) => Err(InvalidStr(index)) + } + WindowsU16s(u16s) => utf16_to_str(u16s) + } + + ## Convert an OS string to a best-effort display string, replacing invalid text + ## with U+FFFD. This representation is lossy and must not be used for roundtripping. + display : OsStr -> Str + display = |os_str| + match to_raw(os_str) { + Utf8(str) => str + UnixBytes(bytes) => Str.from_utf8_lossy(bytes) + WindowsU16s(u16s) => Str.from_utf8_lossy(utf16_to_utf8_lossy(u16s)) + } + + ## Customize debug output for `Str.inspect`. + to_inspect : OsStr -> Str + to_inspect = |os_str| + match to_raw(os_str) { + Utf8(str) => "OsStr.utf8(${Json.to_str(str)})" + UnixBytes(bytes) => + match Str.from_utf8(bytes) { + Ok(str) => "OsStr.unix(${Json.to_str(str)})" + Err(_) => "OsStr.unix_bytes(${Str.inspect(bytes)})" + } + WindowsU16s(u16s) => + match utf16_to_str(u16s) { + Ok(str) => "OsStr.windows(${Json.to_str(str)})" + Err(_) => "OsStr.windows_u16s(${Str.inspect(u16s)})" + } + } + + ## Compare OS strings by their exact tagged representation. + is_eq : OsStr, OsStr -> Bool + is_eq = |left, right| to_raw(left) == to_raw(right) + + ## Hash OS strings consistently with exact tagged equality. + to_hash : OsStr, Hasher -> Hasher + to_hash = |os_str, hasher| + match to_raw(os_str) { + Utf8(str) => Str.to_hash(str, Hasher.write_u8(hasher, 0)) + UnixBytes(bytes) => List.to_hash(bytes, Hasher.write_u8(hasher, 1)) + WindowsU16s(u16s) => List.to_hash(u16s, Hasher.write_u8(hasher, 2)) + } + + ## Expose the host ABI representation. + to_raw : OsStr -> [Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))] + to_raw = |os_str| + match os_str { + Utf8(str) => Utf8(str) + UnixBytes(bytes) => UnixBytes(bytes) + WindowsU16s(u16s) => WindowsU16s(u16s) + } + + ## Build an OS string from the host ABI representation. + from_raw : [Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))] -> OsStr + from_raw = |raw| + match raw { + Utf8(str) => Utf8(str) + UnixBytes(bytes) => UnixBytes(bytes) + WindowsU16s(u16s) => WindowsU16s(u16s) + } + +} + +str_to_utf16 : Str -> List(U16) +str_to_utf16 = |str| utf8_to_utf16(Str.to_utf8(str), []) + +utf8_to_utf16 : List(U8), List(U16) -> List(U16) +utf8_to_utf16 = |remaining, out| + match remaining { + [] => out + [byte, .. as rest] if byte < 0x80 => + utf8_to_utf16(rest, out.append(U8.to_u16(byte))) + [byte1, byte2, .. as rest] if byte1 < 0xE0 => { + top = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte1, 0x1F)), 6) + bottom = U8.to_u32(U8.bitwise_and(byte2, 0x3F)) + utf8_to_utf16(rest, out.append(U32.to_u16_wrap(U32.bitwise_or(top, bottom)))) + } + [byte1, byte2, byte3, .. as rest] if byte1 < 0xF0 => { + top = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte1, 0x0F)), 12) + middle = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte2, 0x3F)), 6) + bottom = U8.to_u32(U8.bitwise_and(byte3, 0x3F)) + code_point = U32.bitwise_or(U32.bitwise_or(top, middle), bottom) + utf8_to_utf16(rest, out.append(U32.to_u16_wrap(code_point))) + } + [byte1, byte2, byte3, byte4, .. as rest] => { + top = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte1, 0x07)), 18) + middle1 = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte2, 0x3F)), 12) + middle2 = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte3, 0x3F)), 6) + bottom = U8.to_u32(U8.bitwise_and(byte4, 0x3F)) + code_point = U32.bitwise_or(U32.bitwise_or(U32.bitwise_or(top, middle1), middle2), bottom) + high = U32.to_u16_wrap(0xD800 + U32.shift_right_by(code_point - 0x10000, 10)) + low = U32.to_u16_wrap(0xDC00 + U32.bitwise_and(code_point - 0x10000, 0x3FF)) + utf8_to_utf16(rest, out.append(high).append(low)) + } + _ => { + crash "A Str contained invalid UTF-8. This should never happen." + } + } + +utf16_to_str : List(U16) -> Try(Str, [InvalidStr(U64)]) +utf16_to_str = |u16s| + match utf16_to_utf8(u16s, [], 0) { + Ok(bytes) => + match Str.from_utf8(bytes) { + Ok(str) => Ok(str) + Err(BadUtf8({ index, problem: _ })) => Err(InvalidStr(index)) + } + Err(InvalidUtf16(index)) => Err(InvalidStr(index)) + } + +utf16_to_utf8 : List(U16), List(U8), U64 -> Try(List(U8), [InvalidUtf16(U64)]) +utf16_to_utf8 = |remaining, out, index| + match remaining { + [] => Ok(out) + [high, low, .. as rest] if is_high_surrogate(high) and is_low_surrogate(low) => { + high_bits = U32.shift_left_by(U16.to_u32(high) - 0xD800, 10) + low_bits = U16.to_u32(low) - 0xDC00 + code_point = 0x10000 + high_bits + low_bits + utf16_to_utf8(rest, append_code_point_utf8(out, code_point), index + 2) + } + [unit, ..] if is_surrogate(unit) => Err(InvalidUtf16(index)) + [unit, .. as rest] => + utf16_to_utf8(rest, append_code_point_utf8(out, U16.to_u32(unit)), index + 1) + } + +utf16_to_utf8_lossy : List(U16) -> List(U8) +utf16_to_utf8_lossy = |u16s| utf16_to_utf8_lossy_help(u16s, []) + +utf16_to_utf8_lossy_help : List(U16), List(U8) -> List(U8) +utf16_to_utf8_lossy_help = |remaining, out| + match remaining { + [] => out + [high, low, .. as rest] if is_high_surrogate(high) and is_low_surrogate(low) => { + high_bits = U32.shift_left_by(U16.to_u32(high) - 0xD800, 10) + low_bits = U16.to_u32(low) - 0xDC00 + code_point = 0x10000 + high_bits + low_bits + utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, code_point)) + } + [unit, .. as rest] if is_surrogate(unit) => + utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, 0xFFFD)) + [unit, .. as rest] => + utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, U16.to_u32(unit))) + } + +append_code_point_utf8 : List(U8), U32 -> List(U8) +append_code_point_utf8 = |out, code_point| + if code_point < 0x80 { + out.append(U32.to_u8_wrap(code_point)) + } else if code_point < 0x800 { + out.append(U32.to_u8_wrap(0xC0 + U32.shift_right_by(code_point, 6))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(code_point, 0x3F))) + } else if code_point < 0x10000 { + out.append(U32.to_u8_wrap(0xE0 + U32.shift_right_by(code_point, 12))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(U32.shift_right_by(code_point, 6), 0x3F))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(code_point, 0x3F))) + } else { + out.append(U32.to_u8_wrap(0xF0 + U32.shift_right_by(code_point, 18))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(U32.shift_right_by(code_point, 12), 0x3F))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(U32.shift_right_by(code_point, 6), 0x3F))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(code_point, 0x3F))) + } + +is_high_surrogate : U16 -> Bool +is_high_surrogate = |unit| unit >= 0xD800 and unit <= 0xDBFF + +is_low_surrogate : U16 -> Bool +is_low_surrogate = |unit| unit >= 0xDC00 and unit <= 0xDFFF + +is_surrogate : U16 -> Bool +is_surrogate = |unit| unit >= 0xD800 and unit <= 0xDFFF + +## Inspection identifies the representation and preserves invalid raw units. +expect Str.inspect(OsStr.utf8("a\nb")) == "OsStr.utf8(\"a\\nb\")" +expect Str.inspect(OsStr.unix("abc")) == "OsStr.unix(\"abc\")" +expect Str.inspect(OsStr.unix_bytes([97, 255, 98])) == "OsStr.unix_bytes([97, 255, 98])" +expect Str.inspect(OsStr.windows("abc")) == "OsStr.windows(\"abc\")" +expect Str.inspect(OsStr.windows_u16s([0xD800, 97])) == "OsStr.windows_u16s([55296, 97])" + +## Interpolation creates a UTF-8 representation. +expect { + name = "config" + value : OsStr + value = "${name}.toml" + value == OsStr.utf8("config.toml") +} + +## Equality and hashing preserve representation identity. +expect OsStr.utf8("abc") != OsStr.unix("abc") +expect Dict.single(OsStr.unix_bytes([97, 255]), "found").get(OsStr.unix_bytes([97, 255])) == Ok("found") diff --git a/platform/Path.roc b/platform/Path.roc index 58dc7226..9fa0d4f3 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -1,417 +1,672 @@ -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 OsStr exposing [OsStr] + +## Construct and operate on byte-preserving paths. Native Unix +## bytes and Windows UTF-16 units are preserved across host effects; use +## `display` only when a lossy human-readable representation is appropriate. +Path := [ + Utf8(Str), + Unix(List(U8)), + Windows(List(U16)), +].{ + + ## Create a path from an OS string value, preserving raw OS units. + from_os_str : OsStr -> Path + from_os_str = |os_str| from_raw(OsStr.to_raw(os_str)) + + ## Convert a path to an OS string value, preserving raw OS units. + to_os_str : Path -> OsStr + to_os_str = |path| OsStr.from_raw(to_raw(path)) + + ## Returns `Bool.True` if the path exists on disk and is pointing at a regular file. + ## + ## This function will traverse symbolic links to query information about the + ## destination file. In case of broken symbolic links this will return `Bool.False`. + is_file! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_file! = |path| + match type!(path) { + Ok(IsFile) => Ok(Bool.True) + Ok(_) => Ok(Bool.False) + Err(PathErr(NotFound)) => Ok(Bool.False) + Err(err) => Err(err) + } + + ## Returns `Bool.True` if the path exists on disk and is pointing at a directory. + ## + ## This function will traverse symbolic links to query information about the + ## destination file. In case of broken symbolic links this will return `Bool.False`. + is_dir! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_dir! = |path| + match type!(path) { + Ok(IsDir) => Ok(Bool.True) + Ok(_) => Ok(Bool.False) + Err(PathErr(NotFound)) => Ok(Bool.False) + Err(err) => Err(err) + } + + ## Returns `Bool.True` if the path exists on disk and is pointing at a symbolic link. + ## + ## This function will not traverse symbolic links - it checks whether the path + ## itself is a symlink. + is_sym_link! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_sym_link! = |path| + match type!(path) { + Ok(IsSymLink) => Ok(Bool.True) + Ok(_) => Ok(Bool.False) + Err(PathErr(NotFound)) => Ok(Bool.False) + Err(err) => Err(err) + } + + ## Returns `True` if the path exists on disk. + exists! : Path => Try(Bool, [PathErr(IOErr), ..]) + exists! = |path| + match type!(path) { + Ok(_) => Ok(Bool.True) + Err(PathErr(NotFound)) => Ok(Bool.False) + Err(err) => Err(err) + } + + ## Return the type of the path if the path exists on disk. + ## + type! : Path => Try([IsFile, IsDir, IsSymLink], [PathErr(IOErr), ..]) + type! = |path| { + Host.path_type!(to_raw(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 + } + }, + ) + } + + ## Read all bytes from a file at this path. + read_bytes! : Path => Try(List(U8), [PathErr(IOErr), ..]) + read_bytes! = |path| map_file_result(Host.file_read_bytes!(to_raw(path))) + + ## Write bytes to a file at this path, replacing any existing contents. + write_bytes! : Path, List(U8) => Try({}, [PathErr(IOErr), ..]) + write_bytes! = |path, bytes| map_file_result(Host.file_write_bytes!(to_raw(path), bytes)) + + ## Read a UTF-8 file at this path. + read_utf8! : Path => Try(Str, [PathErr(IOErr), ..]) + read_utf8! = |path| map_file_result(Host.file_read_utf8!(to_raw(path))) + + ## Write a UTF-8 file at this path, replacing any existing contents. + write_utf8! : Path, Str => Try({}, [PathErr(IOErr), ..]) + write_utf8! = |path, content| map_file_result(Host.file_write_utf8!(to_raw(path), content)) + + ## Delete a file at this path. + delete! : Path => Try({}, [PathErr(IOErr), ..]) + delete! = |path| map_file_result(Host.file_delete!(to_raw(path))) + + ## Return the size of the file at this path in bytes. + size_in_bytes! : Path => Try(U64, [PathErr(IOErr), ..]) + size_in_bytes! = |path| map_file_result(Host.file_size_in_bytes!(to_raw(path))) + + ## Check whether the file at this path has any executable bit set. + is_executable! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_executable! = |path| map_file_result(Host.file_is_executable!(to_raw(path))) + + ## Check whether the file at this path has a readable owner permission bit set. + is_readable! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_readable! = |path| map_file_result(Host.file_is_readable!(to_raw(path))) + + ## Check whether the file at this path has a writable owner permission bit set. + is_writable! : Path => Try(Bool, [PathErr(IOErr), ..]) + is_writable! = |path| map_file_result(Host.file_is_writable!(to_raw(path))) + + ## Return the last accessed time as nanoseconds since the Unix epoch. + time_accessed! : Path => Try(U128, [PathErr(IOErr), ..]) + time_accessed! = |path| map_file_result(Host.file_time_accessed!(to_raw(path))) + + ## Return the last modified time as nanoseconds since the Unix epoch. + time_modified! : Path => Try(U128, [PathErr(IOErr), ..]) + time_modified! = |path| map_file_result(Host.file_time_modified!(to_raw(path))) + + ## Return the creation time as nanoseconds since the Unix epoch. + time_created! : Path => Try(U128, [PathErr(IOErr), ..]) + time_created! = |path| map_file_result(Host.file_time_created!(to_raw(path))) + + ## Create a hard link at `link` pointing to `original`. + hard_link! : Path, Path => Try({}, [PathErr(IOErr), ..]) + hard_link! = |original, link| + map_file_result(Host.file_hard_link!(to_raw(original), to_raw(link))) + + ## Rename a file from `from` to `to`. + rename! : Path, Path => Try({}, [PathErr(IOErr), ..]) + rename! = |from, to| + map_file_result(Host.file_rename!(to_raw(from), to_raw(to))) + + ## Create a directory at this path. + create_dir! : Path => Try({}, [PathErr(IOErr), ..]) + create_dir! = |path| map_dir_result(Host.dir_create!(to_raw(path))) + + ## Create a directory and any missing parent directories at this path. + create_all! : Path => Try({}, [PathErr(IOErr), ..]) + create_all! = |path| map_dir_result(Host.dir_create_all!(to_raw(path))) + + ## Delete an empty directory at this path. + delete_empty! : Path => Try({}, [PathErr(IOErr), ..]) + delete_empty! = |path| map_dir_result(Host.dir_delete_empty!(to_raw(path))) + + ## Delete a directory and all contents at this path. + delete_all! : Path => Try({}, [PathErr(IOErr), ..]) + delete_all! = |path| map_dir_result(Host.dir_delete_all!(to_raw(path))) + + ## List the entries in the directory at this path. + list! : Path => Try(List(Path), [PathErr(IOErr), ..]) + list! = |path| + match Host.dir_list!(to_raw(path)) { + Ok(paths) => Ok(paths.map(from_raw)) + Err(DirErr(err)) => Err(PathErr(err)) + } + + ## Create a UTF-8 text path. + utf8 : Str -> Path + utf8 = |str| Utf8(str) + + ## Create a UTF-8 text path from a quoted literal. + from_quote : Str -> Try(Path, [BadQuotedBytes(Str)]) + from_quote = |str| Ok(Utf8(str)) + + ## Create a UTF-8 path from an interpolated string literal. + ## This performs textual concatenation; use [join] for path-component joining. + from_interpolation : Str, Iter((Str, Str)) -> Path + from_interpolation = |first, rest| + Utf8(rest.fold(first, |acc, (interpolated, segment)| acc.concat(interpolated).concat(segment))) + + ## TODO: Restore generic parser_for and encoder_for helpers when the compiler + ## no longer treats auto-derived `_` declarations in platforms as hosted: + ## https://github.com/roc-lang/roc/issues/10162 + + ## Create a Unix path from a Roc string by storing its UTF-8 bytes. + unix : Str -> Path + unix = |str| Unix(Str.to_utf8(str)) + + ## Create a Unix path from raw bytes without validating UTF-8. + unix_bytes : List(U8) -> Path + unix_bytes = |bytes| Unix(bytes) + + ## Create a Windows path from a Roc string by storing its UTF-16 code units. + windows : Str -> Path + windows = |str| Windows(str_to_utf16(str)) + + ## Create a Windows path from raw UTF-16 code units. + windows_u16s : List(U16) -> Path + windows_u16s = |list| Windows(list) + + ## Convert a path to a string if its raw representation is valid text. + to_str : Path -> Try(Str, [InvalidStr(U64)]) + to_str = |path| + match path { + Utf8(str) => Ok(str) + + Unix(bytes) => + match Str.from_utf8(bytes) { + Ok(str) => Ok(str) + Err(BadUtf8({ index, problem: _ })) => Err(InvalidStr(index)) + } + + Windows(u16s) => utf16_to_str(u16s) + } + + ## Convert a path to a best-effort display string, replacing invalid text with + ## U+FFFD. This representation is lossy and must not be used for roundtripping. + display : Path -> Str + display = |path| + match path { + Utf8(str) => str + Unix(bytes) => Str.from_utf8_lossy(bytes) + Windows(u16s) => Str.from_utf8_lossy(utf16_to_utf8_lossy(u16s)) + } + + ## Render a path for debugging without losing raw OS units. + to_inspect : Path -> Str + to_inspect = |path| + match path { + Utf8(str) => "Path.utf8(${Json.to_str(str)})" + Unix(bytes) => + match Str.from_utf8(bytes) { + Ok(str) => "Path.unix(${Json.to_str(str)})" + Err(_) => "Path.unix_bytes(${Str.inspect(bytes)})" + } + Windows(u16s) => + match utf16_to_str(u16s) { + Ok(str) => "Path.windows(${Json.to_str(str)})" + Err(_) => "Path.windows_u16s(${Str.inspect(u16s)})" + } + } + + ## Compare paths by their exact tagged representation. + is_eq : Path, Path -> Bool + is_eq = |left, right| to_raw(left) == to_raw(right) + + ## Hash paths consistently with exact tagged equality. + to_hash : Path, Hasher -> Hasher + to_hash = |path, hasher| + match to_raw(path) { + Utf8(str) => Str.to_hash(str, Hasher.write_u8(hasher, 0)) + UnixBytes(bytes) => List.to_hash(bytes, Hasher.write_u8(hasher, 1)) + WindowsU16s(u16s) => List.to_hash(u16s, Hasher.write_u8(hasher, 2)) + } + + ## Returns everything after the last directory separator. + filename : Path -> Try(Path, [IsDirPath, EndsInDots]) + filename = |path| + match path { + Utf8(str) => { + bytes = Str.to_utf8(str) + + if ends_with_u8(bytes, '/') { + Err(IsDirPath) + } else if ends_with_two_u8(bytes, '.', '.') { + Err(EndsInDots) + } else { + match List.find_last_index(bytes, |byte| byte == '/') { + Ok(last_sep_index) => Ok(Utf8(str_from_valid_utf8(after_index_u8(bytes, last_sep_index)))) + Err(NotFound) => Ok(path) + } + } + } + + Unix(bytes) => + if ends_with_u8(bytes, '/') { + Err(IsDirPath) + } else if ends_with_two_u8(bytes, '.', '.') { + Err(EndsInDots) + } else { + match List.find_last_index(bytes, |byte| byte == '/') { + Ok(last_sep_index) => Ok(Unix(after_index_u8(bytes, last_sep_index))) + Err(NotFound) => Ok(path) + } + } + + Windows(u16s) => + if ends_with_u16(u16s, '/') or ends_with_u16(u16s, '\\') { + Err(IsDirPath) + } else if ends_with_two_u16(u16s, '.', '.') { + Err(EndsInDots) + } else { + match List.find_last_index(u16s, |u16| u16 == '/' or u16 == '\\') { + Ok(last_sep_index) => Ok(Windows(after_index_u16(u16s, last_sep_index))) + Err(NotFound) => Ok(path) + } + } + } + + ## Returns the filename extension without the leading dot. + ext : Path -> Try(Path, [IsDirPath, EndsInDots]) + ext = |path| + match filename(path) { + Err(err) => Err(err) + Ok(Utf8(str)) => Ok(Utf8(str_from_valid_utf8(ext_units_u8(Str.to_utf8(str))))) + Ok(Unix(bytes)) => Ok(Unix(ext_units_u8(bytes))) + Ok(Windows(u16s)) => Ok(Windows(ext_units_u16(u16s))) + } + + ## Adds a separator and a string component to the path. + join : Path, Str -> Path + join = |path, str| + match path { + Utf8(path_str) => Utf8(path_str.concat("/").concat(str)) + Unix(bytes) => Unix(bytes.append('/').concat(Str.to_utf8(str))) + Windows(u16s) => Windows(u16s.append('\\').concat(str_to_utf16(str))) + } + + ## Expose the raw OS-specific representation. + to_raw : Path -> [Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))] + to_raw = |path| + match path { + Utf8(str) => Utf8(str) + Unix(bytes) => UnixBytes(bytes) + Windows(u16s) => WindowsU16s(u16s) + } + + ## Build a path from the raw OS-specific representation. + from_raw : [Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))] -> Path + from_raw = |raw| + match raw { + Utf8(str) => Utf8(str) + UnixBytes(bytes) => Unix(bytes) + WindowsU16s(u16s) => Windows(u16s) + } +} + +map_file_result : Try(a, [FileErr(IOErr)]) -> Try(a, [PathErr(IOErr), ..]) +map_file_result = |result| + match result { + Ok(value) => Ok(value) + Err(FileErr(err)) => Err(PathErr(err)) + } + +map_dir_result : Try(a, [DirErr(IOErr)]) -> Try(a, [PathErr(IOErr), ..]) +map_dir_result = |result| + match result { + Ok(value) => Ok(value) + Err(DirErr(err)) => Err(PathErr(err)) + } + +str_from_valid_utf8 : List(U8) -> Str +str_from_valid_utf8 = |bytes| + match Str.from_utf8(bytes) { + Ok(str) => str + Err(_) => { + crash "A valid UTF-8 path contained invalid UTF-8 after ASCII path slicing." + } + } + +str_to_utf16 : Str -> List(U16) +str_to_utf16 = |str| utf8_to_utf16(Str.to_utf8(str), []) + +utf8_to_utf16 : List(U8), List(U16) -> List(U16) +utf8_to_utf16 = |remaining, out| + match remaining { + [] => out + + [byte, .. as rest] if byte < 0x80 => + utf8_to_utf16(rest, out.append(U8.to_u16(byte))) + + [byte1, byte2, .. as rest] if byte1 < 0xE0 => { + top = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte1, 0x1F)), 6) + bottom = U8.to_u32(U8.bitwise_and(byte2, 0x3F)) + code_point = U32.bitwise_or(top, bottom) + + utf8_to_utf16(rest, out.append(U32.to_u16_wrap(code_point))) + } + + [byte1, byte2, byte3, .. as rest] if byte1 < 0xF0 => { + top = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte1, 0x0F)), 12) + middle = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte2, 0x3F)), 6) + bottom = U8.to_u32(U8.bitwise_and(byte3, 0x3F)) + code_point = U32.bitwise_or(U32.bitwise_or(top, middle), bottom) + + utf8_to_utf16(rest, out.append(U32.to_u16_wrap(code_point))) + } + + [byte1, byte2, byte3, byte4, .. as rest] => { + top = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte1, 0x07)), 18) + middle1 = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte2, 0x3F)), 12) + middle2 = U32.shift_left_by(U8.to_u32(U8.bitwise_and(byte3, 0x3F)), 6) + bottom = U8.to_u32(U8.bitwise_and(byte4, 0x3F)) + upper = U32.bitwise_or(U32.bitwise_or(top, middle1), middle2) + code_point = U32.bitwise_or(upper, bottom) + + high = U32.to_u16_wrap(0xD800 + U32.shift_right_by(code_point - 0x10000, 10)) + low = U32.to_u16_wrap(0xDC00 + U32.bitwise_and(code_point - 0x10000, 0x3FF)) + + utf8_to_utf16(rest, out.append(high).append(low)) + } + + _ => { + crash "A Str contained invalid UTF-8. This should never happen." + } + } + +utf16_to_str : List(U16) -> Try(Str, [InvalidStr(U64)]) +utf16_to_str = |u16s| + match utf16_to_utf8(u16s, [], 0) { + Ok(bytes) => + match Str.from_utf8(bytes) { + Ok(str) => Ok(str) + Err(BadUtf8({ index, problem: _ })) => Err(InvalidStr(index)) + } + + Err(InvalidUtf16(index)) => Err(InvalidStr(index)) + } + +utf16_to_utf8 : List(U16), List(U8), U64 -> Try(List(U8), [InvalidUtf16(U64)]) +utf16_to_utf8 = |remaining, out, index| + match remaining { + [] => Ok(out) + + [high, low, .. as rest] if is_high_surrogate(high) and is_low_surrogate(low) => { + high_bits = U32.shift_left_by(U16.to_u32(high) - 0xD800, 10) + low_bits = U16.to_u32(low) - 0xDC00 + code_point = 0x10000 + high_bits + low_bits + + utf16_to_utf8(rest, append_code_point_utf8(out, code_point), index + 2) + } + + [unit, ..] if is_surrogate(unit) => Err(InvalidUtf16(index)) + + [unit, .. as rest] => + utf16_to_utf8(rest, append_code_point_utf8(out, U16.to_u32(unit)), index + 1) + } + +utf16_to_utf8_lossy : List(U16) -> List(U8) +utf16_to_utf8_lossy = |u16s| utf16_to_utf8_lossy_help(u16s, []) + +utf16_to_utf8_lossy_help : List(U16), List(U8) -> List(U8) +utf16_to_utf8_lossy_help = |remaining, out| + match remaining { + [] => out + + [high, low, .. as rest] if is_high_surrogate(high) and is_low_surrogate(low) => { + high_bits = U32.shift_left_by(U16.to_u32(high) - 0xD800, 10) + low_bits = U16.to_u32(low) - 0xDC00 + code_point = 0x10000 + high_bits + low_bits + + utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, code_point)) + } + + [unit, .. as rest] if is_surrogate(unit) => + utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, 0xFFFD)) + + [unit, .. as rest] => + utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, U16.to_u32(unit))) + } + +append_code_point_utf8 : List(U8), U32 -> List(U8) +append_code_point_utf8 = |out, code_point| + if code_point < 0x80 { + out.append(U32.to_u8_wrap(code_point)) + } else if code_point < 0x800 { + out.append(U32.to_u8_wrap(0xC0 + U32.shift_right_by(code_point, 6))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(code_point, 0x3F))) + } else if code_point < 0x10000 { + out.append(U32.to_u8_wrap(0xE0 + U32.shift_right_by(code_point, 12))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(U32.shift_right_by(code_point, 6), 0x3F))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(code_point, 0x3F))) + } else { + out.append(U32.to_u8_wrap(0xF0 + U32.shift_right_by(code_point, 18))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(U32.shift_right_by(code_point, 12), 0x3F))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(U32.shift_right_by(code_point, 6), 0x3F))) + .append(U32.to_u8_wrap(0x80 + U32.bitwise_and(code_point, 0x3F))) + } + +is_high_surrogate : U16 -> Bool +is_high_surrogate = |unit| unit >= 0xD800 and unit <= 0xDBFF + +is_low_surrogate : U16 -> Bool +is_low_surrogate = |unit| unit >= 0xDC00 and unit <= 0xDFFF + +is_surrogate : U16 -> Bool +is_surrogate = |unit| unit >= 0xD800 and unit <= 0xDFFF + +ends_with_u8 : List(U8), U8 -> Bool +ends_with_u8 = |list, suffix| + match list { + [.., last] => last == suffix + [] => False + } + +ends_with_u16 : List(U16), U16 -> Bool +ends_with_u16 = |list, suffix| + match list { + [.., last] => last == suffix + [] => False + } + +ends_with_two_u8 : List(U8), U8, U8 -> Bool +ends_with_two_u8 = |list, first_suffix, second_suffix| + match list { + [.., first, second] => first == first_suffix and second == second_suffix + _ => False + } + +ends_with_two_u16 : List(U16), U16, U16 -> Bool +ends_with_two_u16 = |list, first_suffix, second_suffix| + match list { + [.., first, second] => first == first_suffix and second == second_suffix + _ => False + } + +after_index_u8 : List(U8), U64 -> List(U8) +after_index_u8 = |list, index| { + start = index + 1 + List.sublist(list, { start, len: List.len(list) - start }) +} + +after_index_u16 : List(U16), U64 -> List(U16) +after_index_u16 = |list, index| { + start = index + 1 + List.sublist(list, { start, len: List.len(list) - start }) +} + +ext_units_u8 : List(U8) -> List(U8) +ext_units_u8 = |units| + match List.find_first_index(units, |unit| unit == '.') { + Err(NotFound) => [] + Ok(0) => { + rest = List.drop_first(units, 1) + + match List.find_first_index(rest, |unit| unit == '.') { + Err(NotFound) => [] + Ok(dot_index) => after_index_u8(rest, dot_index) + } + } + Ok(dot_index) => after_index_u8(units, dot_index) + } + +ext_units_u16 : List(U16) -> List(U16) +ext_units_u16 = |units| + match List.find_first_index(units, |unit| unit == '.') { + Err(NotFound) => [] + Ok(0) => { + rest = List.drop_first(units, 1) + + match List.find_first_index(rest, |unit| unit == '.') { + Err(NotFound) => [] + Ok(dot_index) => after_index_u16(rest, dot_index) + } + } + Ok(dot_index) => after_index_u16(units, dot_index) + } + +quoted_literal_path : Path +quoted_literal_path = "config.txt" + +path_identity : Path -> Path +path_identity = |path| path + +## Constructors preserve Unix, Windows, and UTF-8 path representations. +expect Path.unix("abc") == Unix([97, 98, 99]) +expect Path.unix_bytes([97, 98, 99]) == Unix([97, 98, 99]) +expect Path.windows("abc") == Windows([97, 98, 99]) +expect Path.windows_u16s([97, 98, 99]) == Windows([97, 98, 99]) +expect Path.utf8("abc") == Utf8("abc") + +## Quoted literals dispatch to UTF-8 paths through `from_quote`. +expect Path.from_quote("config.txt") == Ok(Path.utf8("config.txt")) +expect quoted_literal_path == Path.utf8("config.txt") +expect path_identity("nested/config.txt") == Path.utf8("nested/config.txt") + +## Interpolation creates a UTF-8 representation. +expect { + directory = "config" + path : Path + path = "${directory}/app.toml" + path == Path.utf8("config/app.toml") +} + +## Raw conversion roundtrips every representation without validating raw OS data. +expect Path.to_raw(Path.unix_bytes([97, 255, 98])) == UnixBytes([97, 255, 98]) +expect Path.to_raw(Path.windows_u16s([0xD800, 97])) == WindowsU16s([0xD800, 97]) +expect Path.to_raw(Path.utf8("abc")) == Utf8("abc") +expect Path.from_raw(UnixBytes([97, 255, 98])) == Path.unix_bytes([97, 255, 98]) +expect Path.from_raw(WindowsU16s([97, 98, 99])) == Path.windows("abc") +expect Path.from_raw(Utf8("abc")) == Path.utf8("abc") + +## `to_str` succeeds for valid text and reports the first invalid raw unit. +expect Path.to_str(Path.unix("abc")) == Ok("abc") +expect Path.to_str(Path.unix_bytes([97, 255, 98])) == Err(InvalidStr(1)) +expect Path.to_str(Path.windows("abc")) == Ok("abc") +expect Path.to_str(Path.windows_u16s([0xD800])) == Err(InvalidStr(0)) +expect Path.to_str(Path.utf8("abc")) == Ok("abc") +expect Path.to_str(Path.windows_u16s([0xD83D, 0xDC26])) == Ok(Str.from_utf8_lossy([0xF0, 0x9F, 0x90, 0xA6])) + +## `display` preserves valid text and replaces invalid raw units. +expect Path.display(Path.unix("abc")) == "abc" +expect Path.display(Path.unix_bytes([97, 255, 98])) == Str.from_utf8_lossy([97, 255, 98]) +expect Path.display(Path.windows("abc")) == "abc" +expect Path.display(Path.windows_u16s([0xD800, 97])) == Str.from_utf8_lossy([0xEF, 0xBF, 0xBD, 97]) +expect Path.display(Path.utf8("abc")) == "abc" + +## Inspection identifies the representation and preserves invalid raw units. +expect Str.inspect(Path.utf8("a\nb")) == "Path.utf8(\"a\\nb\")" +expect Str.inspect(Path.unix("abc")) == "Path.unix(\"abc\")" +expect Str.inspect(Path.unix_bytes([97, 255, 98])) == "Path.unix_bytes([97, 255, 98])" +expect Str.inspect(Path.windows("abc")) == "Path.windows(\"abc\")" +expect Str.inspect(Path.windows_u16s([0xD800, 97])) == "Path.windows_u16s([55296, 97])" + +## Equality and hashing preserve representation identity. +expect Path.utf8("abc") != Path.unix("abc") +expect Dict.single(Path.unix_bytes([97, 255]), "found").get(Path.unix_bytes([97, 255])) == Ok("found") + +## `filename` returns everything after the last separator. +expect Path.filename(Path.unix("foo/bar.txt")) == Ok(Path.unix("bar.txt")) +expect Path.filename(Path.unix("foo/bar")) == Ok(Path.unix("bar")) +expect Path.filename(Path.unix("foo")) == Ok(Path.unix("foo")) +expect Path.filename(Path.unix("")) == Ok(Path.unix("")) +expect Path.filename(Path.windows("foo\\bar.txt")) == Ok(Path.windows("bar.txt")) +expect Path.filename(Path.windows("foo/bar.txt")) == Ok(Path.windows("bar.txt")) +expect Path.filename(Path.windows("foo")) == Ok(Path.windows("foo")) +expect Path.filename(Path.windows("")) == Ok(Path.windows("")) +expect Path.filename(Path.utf8("foo/bar.txt")) == Ok(Path.utf8("bar.txt")) +expect Path.filename(Path.utf8("foo")) == Ok(Path.utf8("foo")) +expect Path.filename(Path.utf8("")) == Ok(Path.utf8("")) + +## `filename` rejects directory paths and filenames ending in two dots. +expect Path.filename(Path.unix("foo/bar/")) == Err(IsDirPath) +expect Path.filename(Path.unix("foo/bar..")) == Err(EndsInDots) +expect Path.filename(Path.windows("foo\\bar\\")) == Err(IsDirPath) +expect Path.filename(Path.windows("foo/bar..")) == Err(EndsInDots) +expect Path.filename(Path.utf8("foo/bar/")) == Err(IsDirPath) +expect Path.filename(Path.utf8("foo/bar..")) == Err(EndsInDots) + +## `ext` returns the filename extension without the leading dot. +expect Path.ext(Path.unix("foo/bar.txt")) == Ok(Path.unix("txt")) +expect Path.ext(Path.unix("foo/bar.")) == Ok(Path.unix("")) +expect Path.ext(Path.unix("foo/.bar.txt")) == Ok(Path.unix("txt")) +expect Path.ext(Path.unix("foo/bar")) == Ok(Path.unix("")) +expect Path.ext(Path.unix("foo/.bar")) == Ok(Path.unix("")) +expect Path.ext(Path.unix("foo/bar.baz.txt")) == Ok(Path.unix("baz.txt")) +expect Path.ext(Path.unix("foo/.bar.baz.txt")) == Ok(Path.unix("baz.txt")) +expect Path.ext(Path.unix("")) == Ok(Path.unix("")) +expect Path.ext(Path.windows("foo\\bar.txt")) == Ok(Path.windows("txt")) +expect Path.ext(Path.windows("foo\\bar")) == Ok(Path.windows("")) +expect Path.ext(Path.windows("")) == Ok(Path.windows("")) +expect Path.ext(Path.utf8("foo/bar.txt")) == Ok(Path.utf8("txt")) +expect Path.ext(Path.utf8("foo/.bar")) == Ok(Path.utf8("")) +expect Path.ext(Path.utf8("foo/bar.baz.txt")) == Ok(Path.utf8("baz.txt")) +expect Path.ext(Path.utf8("")) == Ok(Path.utf8("")) + +## `ext` forwards filename errors for directory paths and dot endings. +expect Path.ext(Path.unix("foo/bar/")) == Err(IsDirPath) +expect Path.ext(Path.unix("foo/bar..")) == Err(EndsInDots) +expect Path.ext(Path.windows("foo\\bar\\")) == Err(IsDirPath) +expect Path.ext(Path.windows("foo\\bar..")) == Err(EndsInDots) +expect Path.ext(Path.utf8("foo/bar/")) == Err(IsDirPath) +expect Path.ext(Path.utf8("foo/bar..")) == Err(EndsInDots) + +## `join` appends a representation-specific separator and text component. +expect Path.join(Path.unix("foo"), "bar") == Path.unix("foo/bar") +expect Path.join(Path.windows("foo"), "bar") == Path.windows("foo\\bar") +expect Path.join(Path.utf8("foo"), "bar") == Path.utf8("foo/bar") diff --git a/platform/Random.roc b/platform/Random.roc index ac11df2b..35ac33a0 100644 --- a/platform/Random.roc +++ b/platform/Random.roc @@ -1,52 +1,21 @@ -module [ - IOErr, - random_seed_u64!, - random_seed_u32!, -] - -import InternalIOErr +import IOErr exposing [IOErr] import Host +## Obtain random seed values from the operating system's entropy source. +Random :: [].{ -## 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 64-bit unsigned integer seed. + seed_u64! : () => Try(U64, [RandomErr(IOErr), ..]) + seed_u64! = || widen_random_err(Host.random_seed_u64!()) -## 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 32-bit unsigned integer seed. + seed_u32! : () => Try(U32, [RandomErr(IOErr), ..]) + seed_u32! = || widen_random_err(Host.random_seed_u32!()) +} -## 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 +widen_random_err : Try(a, [RandomErr(IOErr)]) -> Try(a, [RandomErr(IOErr), ..]) +widen_random_err = |result| + match result { + Ok(value) => Ok(value) + Err(RandomErr(err)) => Err(RandomErr(err)) + } diff --git a/platform/Sleep.roc b/platform/Sleep.roc index 68f97966..a147a951 100644 --- a/platform/Sleep.roc +++ b/platform/Sleep.roc @@ -1,12 +1,9 @@ -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) +## Pause the current process for a requested duration. +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..42b914c7 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -1,802 +1,499 @@ -module [ - Value, - ErrCode, - Binding, - Stmt, - SqlDecodeErr, - query!, - query_many!, - execute!, - prepare!, - query_prepared!, - query_many_prepared!, - execute_prepared!, - errcode_to_str, - decode_record, - map_value, - map_value_result, - tagged_value, - str, - bytes, - i64, - i32, - i16, - i8, - u64, - u32, - u16, - u8, - f64, - f32, - Nullable, - nullable_str, - nullable_bytes, - nullable_i64, - nullable_i32, - nullable_i16, - nullable_i8, - nullable_u64, - nullable_u32, - nullable_u16, - nullable_u8, - nullable_f64, - nullable_f32, -] - -import Host import InternalSqlite - -## Represents a value that can be stored in a Sqlite database. -## -## ``` -## [ -## Null, -## Real F64, -## Integer I64, -## String Str, -## Bytes (List U8), -## ] -## ``` -Value : InternalSqlite.SqliteValue - -## Represents various error codes that can be returned by Sqlite. -## ``` -## [ -## Error, # SQL error or missing database -## Internal, # Internal logic error in Sqlite -## Perm, # Access permission denied -## Abort, # Callback routine requested an abort -## Busy, # The database file is locked -## Locked, # A table in the database is locked -## NoMem, # A malloc() failed -## ReadOnly, # Attempt to write a readonly database -## Interrupt, # Operation terminated by sqlite3_interrupt( -## IOErr, # Some kind of disk I/O error occurred -## Corrupt, # The database disk image is malformed -## NotFound, # Unknown opcode in sqlite3_file_control() -## Full, # Insertion failed because database is full -## CanNotOpen, # Unable to open the database file -## Protocol, # Database lock protocol error -## Empty, # Database is empty -## Schema, # The database schema changed -## TooBig, # String or BLOB exceeds size limit -## Constraint, # Abort due to constraint violation -## Mismatch, # Data type mismatch -## Misuse, # Library used incorrectly -## NoLfs, # Uses OS features not supported on host -## AuthDenied, # Authorization denied -## Format, # Auxiliary database format error -## OutOfRange, # 2nd parameter to sqlite3_bind out of range -## NotADatabase, # File opened that is not a database file -## Notice, # Notifications from sqlite3_log() -## Warning, # Warnings from sqlite3_log() -## Row, # sqlite3_step() has another row ready -## Done, # sqlite3_step() has finished executing -## Unknown I64, # error code not known -## ] -## ``` -ErrCode : [ - Error, # SQL error or missing database - Internal, # Internal logic error in Sqlite - Perm, # Access permission denied - Abort, # Callback routine requested an abort - Busy, # The database file is locked - Locked, # A table in the database is locked - NoMem, # A malloc() failed - ReadOnly, # Attempt to write a readonly database - Interrupt, # Operation terminated by sqlite3_interrupt( - IOErr, # Some kind of disk I/O error occurred - Corrupt, # The database disk image is malformed - NotFound, # Unknown opcode in sqlite3_file_control() - Full, # Insertion failed because database is full - CanNotOpen, # Unable to open the database file - Protocol, # Database lock protocol error - Empty, # Database is empty - Schema, # The database schema changed - TooBig, # String or BLOB exceeds size limit - Constraint, # Abort due to constraint violation - Mismatch, # Data type mismatch - Misuse, # Library used incorrectly - NoLFS, # Uses OS features not supported on host - AuthDenied, # Authorization denied - Format, # Auxiliary database format error - OutOfRange, # 2nd parameter to sqlite3_bind out of range - NotADatabase, # File opened that is not a database file - Notice, # Notifications from sqlite3_log() - Warning, # Warnings from sqlite3_log() - Row, # sqlite3_step() has another row ready - Done, # sqlite3_step() has finished executing - Unknown I64, # error code not known -] - -## Bind a name and a value to pass to the Sqlite database. -## ``` -## { -## name : Str, -## value : SqliteValue, -## } -## ``` -Binding : InternalSqlite.SqliteBindings - -## Represents a prepared statement that can be executed many times. -Stmt := Box {} - -## Prepare a [Stmt] for execution at a later time. -## -## This is useful when you have a query that will be called many times, as it is more efficient than -## preparing the query each time it is called. This is usually done in `init!` with the prepared `Stmt` stored in the model. -## -## ``` -## prepared_query = Sqlite.prepare!({ -## path : "path/to/database.db", -## query : "SELECT * FROM todos;", -## })? -## -## Sqlite.query_many_prepared!({ -## stmt: prepared_query, -## bindings: [], -## rows: { Sqlite.decode_record <- -## id: Sqlite.i64("id"), -## task: Sqlite.str("task"), -## }, -## })? -## ``` -prepare! : - { - path : Str, - query : Str, - } - => 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, - } - => 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, - } - => 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), - } - => 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), - } - => 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, - } - => 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, - } - => 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( - |cols| - decode_first! = gen_first(cols) - decode_second! = gen_second(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( - |cols| - decode! = gen_decode(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( - |cols| - decode! = gen_decode(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| - type = - when val is - 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]. +import Host +import Path + +# 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). +## Execute SQLite statements and decode rows using either one-shot or reusable +## prepared APIs. Application code works with the public `Value`, `Binding`, +## `Stmt`, and `ErrCode` types below; raw host ABI records remain internal. ## -## 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 +## Database paths use basic-cli's byte-preserving `Path` type. +Sqlite :: [].{ + + ## A value accepted by a SQLite binding or returned from a column. + Value : [ + Null, + Real(F64), + Integer(I64), + String(Str), + Bytes(List(U8)), + ] + + ## A named parameter binding. Include SQLite's parameter prefix in `name`, + ## for example `{ name: ":id", value: Integer(42) }`. + Binding : { + name : Str, + value : Value, + } + + ## Represents a prepared statement that can be executed many times. + Stmt :: { host : Host.SqliteStmt }.{ + + ## Render the statement without exposing its host handle. + to_inspect : Stmt -> Str + to_inspect = |_| "Sqlite.Stmt()" + + ## Execute this prepared statement without returning rows. + execute! : Stmt, List(Binding) => Try({}, [RowsReturnedUseQueryInstead, SqliteErr(ErrCode, Str), ..]) + execute! = |stmt, bindings| { + host_stmt = stmt_to_host(stmt) + sqlite_bind!(host_stmt, bindings)? + res = sqlite_step!(host_stmt) + sqlite_reset!(host_stmt)? + match res { + Ok(Done) => Ok({}) + Ok(Row) => Err(RowsReturnedUseQueryInstead) + Err(e) => Err(e) + } + } + + ## Execute this prepared query and decode exactly one row. + query! = |stmt, bindings, decode| { + host_stmt = stmt_to_host(stmt) + sqlite_bind!(host_stmt, bindings)? + res = decode_exactly_one_row!(host_stmt, decode) + sqlite_reset!(host_stmt)? + res + } + + ## Execute this prepared query and decode all returned rows. + query_many! = |stmt, bindings, decode| { + host_stmt = stmt_to_host(stmt) + sqlite_bind!(host_stmt, bindings)? + res = decode_rows!(host_stmt, decode) + sqlite_reset!(host_stmt)? + res + } + } + + ## 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)] + + ## Prepare a `Stmt` for reuse by the prepared execute and query operations. + prepare! : { path : Path.Path, query : Str } => Try(Stmt, [SqliteErr(ErrCode, Str), ..]) + prepare! = |{ path, query: q }| + sqlite_prepare!(Path.to_raw(path), q).map_ok(|stmt| Stmt.{ host: stmt }) + + ## Execute a SQL statement that **doesn't return any rows** (INSERT/UPDATE/DELETE). + execute! : { path : Path.Path, query : Str, bindings : List(Binding) } => Try({}, [RowsReturnedUseQueryInstead, SqliteErr(ErrCode, Str), ..]) + execute! = |{ path, query: q, bindings }| { + stmt = prepare!({ path, query: q })? + stmt.execute!(bindings) + } + + ## Execute a SQL query and decode exactly one row into a value. `bindings` + ## is a `List(Binding)` and `row` is a decoder built from the functions below. + query! = |{ path, query: q, bindings, row }| { + stmt = prepare!({ path, query: q })? + stmt.query!(bindings, row) + } + + ## Execute a SQL query and decode multiple rows into a list of values. + ## `bindings` is a `List(Binding)` and `rows` is a row decoder. + query_many! = |{ path, query: q, bindings, rows }| { + stmt = prepare!({ path, query: q })? + stmt.query_many!(bindings, rows) + } + + # ---- Row decoding combinators ---------------------------------------------- + + ## Decode a Sqlite row into a record by combining two decoders. + decode_record = |gen_first, gen_second, mapper| + |cols| + |stmt| + match gen_first(cols)(stmt) { + Ok(first) => + match gen_second(cols)(stmt) { + Ok(second) => Ok(mapper(first, second)) + Err(e) => Err(e) + } + Err(e) => Err(e) + } + + ## Transform the output of a decoder by applying a function to the decoded value. + map_value = |gen_decode, mapper| + |cols| + |stmt| + match gen_decode(cols)(stmt) { + Ok(val) => Ok(mapper(val)) + Err(e) => Err(e) + } + + ## Transform a decoder's output with a function returning a `Try`. + map_value_result = |gen_decode, mapper| + |cols| + |stmt| + match gen_decode(cols)(stmt) { + Ok(val) => mapper(val) + Err(e) => Err(e) + } + + # ---- Leaf decoders --------------------------------------------------------- + + ## Decode a `Value` keeping it tagged. + tagged_value = |name| + |cols| + |stmt| lookup_value!(cols, stmt, name) + + ## Decode a column to a `Str`. + str = |name| + |cols| + |stmt| + 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| + |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)) + + ## 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) ----------------------------------------- + +stmt_to_host : Sqlite.Stmt -> Host.SqliteStmt +stmt_to_host = |stmt| stmt.host + +sqlite_prepare! = |raw_path, query| + Host.sqlite_prepare!(raw_path, query) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + +sqlite_bind! : Host.SqliteStmt, List({ name : Str, value : [Null, Real(F64), Integer(I64), String(Str), Bytes(List(U8))] }) => Try({}, [SqliteErr(Sqlite.ErrCode, Str), ..]) +sqlite_bind! = |stmt, bindings| + Host.sqlite_bind!(stmt, bindings) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + +sqlite_columns! = |stmt| Host.sqlite_columns!(stmt) + +sqlite_column_value! = |stmt, index| + Host.sqlite_column_value!(stmt, index) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + +sqlite_step! : Host.SqliteStmt => Try([Row, Done], [SqliteErr(Sqlite.ErrCode, Str), ..]) +sqlite_step! = |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)) + } + +sqlite_reset! : Host.SqliteStmt => Try({}, [SqliteErr(Sqlite.ErrCode, Str), ..]) +sqlite_reset! = |stmt| + Host.sqlite_reset!(stmt) + .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) + +decode_exactly_one_row! = |stmt, gen_decode| { + cols = sqlite_columns!(stmt) + decode_row! = gen_decode(cols) + match sqlite_step!(stmt)? { + Row => { + row = decode_row!(stmt)? + match sqlite_step!(stmt)? { + Done => Ok(row) + Row => Err(TooManyRowsReturned) + } + } + Done => Err(NoRowsReturned) + } +} + +decode_rows! = |stmt, gen_decode| { + cols = sqlite_columns!(stmt) + decode_row! = gen_decode(cols) + helper! = |out| + match sqlite_step!(stmt)? { + Done => Ok(out) + Row => { + row = decode_row!(stmt)? + helper!(out.append(row)) + } + } + helper!([]) +} + +lookup_value! = |cols, stmt, name| + match cols.find_first_index(|x| x == name) { + Ok(index) => sqlite_column_value!(stmt, index) + Err(NotFound) => Err(NoSuchField(name)) + } + +nullable_int_decoder = |name, cast| + |cols| + |stmt| + match lookup_value!(cols, stmt, name) { + Ok(Integer(n)) => + match cast(n) { + Ok(v) => Ok(NotNull(v)) + Err(e) => Err(e) + } + Ok(Null) => Ok(Null) + Ok(other) => to_unexpected_type_err(other) + Err(e) => Err(e) + } + +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) + } + +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) + } + +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) + } + +to_unexpected_type_err = |val| { + type = match val { + Integer(_) => Integer + Real(_) => Real + String(_) => String + Bytes(_) => Bytes + Null => Null + } + Err(UnexpectedType(type)) +} + +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..4addeed3 100644 --- a/platform/Stderr.roc +++ b/platform/Stderr.roc @@ -1,72 +1,36 @@ -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, -] +## Write text or raw bytes to the process's standard error stream. +Stderr :: [].{ -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 => Try({}, [StderrErr(IOErr), ..]) + line! = |message| widen_stderr_err(Host.stderr_line!(message)) -## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)), -## followed by a newline. -## -## > To write to `stderr` without the newline, see [Stderr.write!]. -line! : Str => Result {} [StderrErr IOErr] -line! = |str| - Host.stderr_line!(str) - |> Result.map_err(handle_err) + ## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). + ## + ## Most terminals will not actually display strings that are written to them until they receive a newline, + ## so this may appear to do nothing until you write a newline! + ## + ## > To write to `stderr` with a newline at the end, see [Stderr.line!]. + write! : Str => Try({}, [StderrErr(IOErr), ..]) + write! = |message| widen_stderr_err(Host.stderr_write!(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 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| widen_stderr_err(Host.stderr_write_bytes!(bytes)) +} -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 +widen_stderr_err : Try(a, [StderrErr(IOErr)]) -> Try(a, [StderrErr(IOErr), ..]) +widen_stderr_err = |result| + match result { + Ok(value) => Ok(value) + Err(StderrErr(err)) => Err(StderrErr(err)) + } diff --git a/platform/Stdin.roc b/platform/Stdin.roc index 90ad5be6..0c1a3632 100644 --- a/platform/Stdin.roc +++ b/platform/Stdin.roc @@ -1,89 +1,44 @@ -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, -] +## Read lines, chunks, or all remaining bytes from standard input. +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! = || widen_stdin_eof_err(Host.stdin_line!()) -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 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! = || widen_stdin_eof_err(Host.stdin_bytes!()) -## Read a line from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). -## -## > This task will block the program from continuing until `stdin` receives a newline character -## (e.g. because the user pressed Enter in the terminal), so using it can result in the appearance of the -## programming having gotten stuck. It's often helpful to print a prompt first, so -## the user knows it's necessary to enter something before the program will continue. -line! : {} => Result Str [EndOfFile, StdinErr IOErr] -line! = |{}| - Host.stdin_line!({}) - |> Result.map_err(handle_err) + ## Read 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! = || widen_stdin_err(Host.stdin_read_to_end!()) +} -## 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) +widen_stdin_eof_err : Try(a, [EndOfFile, StdinErr(IOErr)]) -> Try(a, [EndOfFile, StdinErr(IOErr), ..]) +widen_stdin_eof_err = |result| + match result { + Ok(value) => Ok(value) + Err(EndOfFile) => Err(EndOfFile) + Err(StdinErr(err)) => Err(StdinErr(err)) + } -## Read all bytes from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)) -## until [EOF](https://en.wikipedia.org/wiki/End-of-file) in this source. -read_to_end! : {} => Result (List U8) [StdinErr IOErr] -read_to_end! = |{}| - Host.stdin_read_to_end!({}) - |> Result.map_err( - |{ tag, msg }| - when tag is - NotFound -> StdinErr(NotFound) - PermissionDenied -> StdinErr(PermissionDenied) - BrokenPipe -> StdinErr(BrokenPipe) - AlreadyExists -> StdinErr(AlreadyExists) - Interrupted -> StdinErr(Interrupted) - Unsupported -> StdinErr(Unsupported) - OutOfMemory -> StdinErr(OutOfMemory) - EndOfFile -> crash("unreachable, reading to EOF") - Other -> StdinErr(Other(msg)), - ) +widen_stdin_err : Try(a, [StdinErr(IOErr)]) -> Try(a, [StdinErr(IOErr), ..]) +widen_stdin_err = |result| + match result { + Ok(value) => Ok(value) + Err(StdinErr(err)) => Err(StdinErr(err)) + } diff --git a/platform/Stdout.roc b/platform/Stdout.roc index 569299d2..c8b48498 100644 --- a/platform/Stdout.roc +++ b/platform/Stdout.roc @@ -1,73 +1,36 @@ -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, -] +## Write text or raw bytes to the process's standard output stream. +Stdout :: [].{ -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 => Try({}, [StdoutErr(IOErr), ..]) + line! = |message| widen_stdout_err(Host.stdout_line!(message)) -## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), -## followed by a newline. -## -## > To write to `stdout` without the newline, see [Stdout.write!]. -## -line! : Str => Result {} [StdoutErr IOErr] -line! = |str| - Host.stdout_line!(str) - |> Result.map_err(handle_err) + ## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). + ## + ## Note that many terminals will not actually display strings that are written to them until they receive a newline, + ## so this may appear to do nothing until you write a newline! + ## + ## > To write to `stdout` with a newline at the end, see [Stdout.line!]. + write! : Str => Try({}, [StdoutErr(IOErr), ..]) + write! = |message| widen_stdout_err(Host.stdout_write!(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 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| widen_stdout_err(Host.stdout_write_bytes!(bytes)) +} -write_bytes! : List U8 => Result {} [StdoutErr IOErr] -write_bytes! = |bytes| - Host.stdout_write_bytes!(bytes) - |> Result.map_err(handle_err) +widen_stdout_err : Try(a, [StdoutErr(IOErr)]) -> Try(a, [StdoutErr(IOErr), ..]) +widen_stdout_err = |result| + match result { + Ok(value) => Ok(value) + Err(StdoutErr(err)) => Err(StdoutErr(err)) + } diff --git a/platform/Tcp.roc b/platform/Tcp.roc index 724126d1..4fd39570 100644 --- a/platform/Tcp.roc +++ b/platform/Tcp.roc @@ -1,225 +1,152 @@ -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 +## Connect to TCP servers and exchange buffered byte streams. +Tcp :: [].{ + + ## Represents a TCP stream. + ## + ## The connection is automatically closed when the last reference to the + ## stream is dropped. It wraps an opaque host-side `BufReader` + ## handle. + Stream :: { host : Host.TcpStream }.{ + + ## Render the stream without exposing its host handle. + to_inspect : Stream -> Str + to_inspect = |_| "Tcp.Stream()" + + ## Read up to a number of bytes from this TCP stream. + read_up_to! : Stream, U64 => Try(List(U8), _) + read_up_to! = |stream, bytes_to_read| + Host.tcp_read_up_to!(stream.host, bytes_to_read) + .map_err(|err| TcpReadErr(parse_stream_err(err))) + + ## Read an exact number of bytes or fail. + ## + ## `TcpUnexpectedEOF` is returned if the stream ends before the specified + ## number of bytes is reached. + read_exactly! : Stream, U64 => Try(List(U8), _) + read_exactly! = |stream, bytes_to_read| + match Host.tcp_read_exactly!(stream.host, 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. If found, the delimiter is + ## included as the last byte. + read_until! : Stream, U8 => Try(List(U8), _) + read_until! = |stream, byte| + Host.tcp_read_until!(stream.host, byte) + .map_err(|err| TcpReadErr(parse_stream_err(err))) + + ## Read until a newline (`\n`, byte 10) or EOF is reached as UTF-8. + ## If found, the newline is included as the last character. + read_line! : Stream => Try(Str, _) + read_line! = |stream| + # NB: use `match` rather than `?` here — `read_until!` yields a + # single-variant error union and `?` currently miscompiles (roc#9826). + match read_until!(stream, 10) { + Ok(bytes) => Str.from_utf8(bytes).map_err(|err| TcpReadBadUtf8(err)) + Err(err) => Err(err) + } + + ## Write bytes to this TCP stream. + write! : Stream, List(U8) => Try({}, _) + write! = |stream, bytes| + Host.tcp_write!(stream.host, bytes) + .map_err(|err| TcpWriteErr(parse_stream_err(err))) + + ## Write a string to this TCP stream, encoded as UTF-8. + write_utf8! : Stream, Str => Try({}, _) + write_utf8! = |stream, str| write!(stream, Str.to_utf8(str)) + } + + ## Represents errors that can occur when connecting to a remote host. + ConnectErr : [ + PermissionDenied, + AddrInUse, + AddrNotAvailable, + ConnectionRefused, + Interrupted, + TimedOut, + Unsupported, + Unrecognized(Str), + ] + + ## Represents errors that can occur when performing an effect with a `Stream`. + StreamErr : [ + StreamNotFound, + PermissionDenied, + ConnectionRefused, + ConnectionReset, + Interrupted, + OutOfMemory, + BrokenPipe, + Unrecognized(Str), + ] + + ## Opens a TCP connection to a remote host. + ## + ## ```roc + ## # Connect to localhost:8080 + ## stream = Tcp.connect!("localhost", 8080)? + ## ``` + ## + ## Valid hostnames look like `127.0.0.1`, `::1`, `localhost`, or `roc-lang.org`. + connect! = |host, port| + Host.tcp_connect!(host, port) + .map_ok(|stream| Stream.{ host: stream }) + .map_err(parse_connect_err) + + ## 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, -] - -parse_stream_err : Str -> StreamErr + match err { + "ErrorKind::PermissionDenied" => PermissionDenied + "ErrorKind::AddrInUse" => AddrInUse + "ErrorKind::AddrNotAvailable" => AddrNotAvailable + "ErrorKind::ConnectionRefused" => ConnectionRefused + "ErrorKind::Interrupted" => Interrupted + "ErrorKind::TimedOut" => TimedOut + "ErrorKind::Unsupported" => Unsupported + other => Unrecognized(other) + } + parse_stream_err = |err| - when err is - "StreamNotFound" -> StreamNotFound - "ErrorKind::PermissionDenied" -> PermissionDenied - "ErrorKind::ConnectionRefused" -> ConnectionRefused - "ErrorKind::ConnectionReset" -> ConnectionReset - "ErrorKind::Interrupted" -> Interrupted - "ErrorKind::OutOfMemory" -> OutOfMemory - "ErrorKind::BrokenPipe" -> BrokenPipe - other -> Unrecognized(other) - -## Opens a TCP connection to a remote host. -## -## ``` -## # Connect to localhost:8080 -## stream = Tcp.connect!("localhost", 8080)? -## ``` -## -## The connection is automatically closed when the last reference to the stream is dropped. -## Examples of -## valid hostnames: -## - `127.0.0.1` -## - `::1` -## - `localhost` -## - `roc-lang.org` -## -connect! : Str, U16 => Result Stream (ConnectErr _) -connect! = |host, port| - Host.tcp_connect!(host, port) - |> Result.map_ok(@Stream) - |> Result.map_err(parse_connect_err) - -## Read up to a number of bytes from the TCP stream. -## -## ``` -## # Read up to 64 bytes from the stream -## received_bytes = Tcp.read_up_to!(stream, 64)? -## ``` -## -## > To read an exact number of bytes or fail, you can use [Tcp.read_exactly!] instead. -read_up_to! : Stream, U64 => Result (List U8) [TcpReadErr StreamErr] -read_up_to! = |@Stream(stream), bytes_to_read| - Host.tcp_read_up_to!(stream, bytes_to_read) - |> Result.map_err(|err| TcpReadErr(parse_stream_err(err))) - -## Read an exact number of bytes or fail. -## -## ``` -## bytes = Tcp.read_exactly!(stream, 64)? -## ``` -## -## `TcpUnexpectedEOF` is returned if the stream ends before the specfied number of bytes is reached. -## -read_exactly! : Stream, U64 => Result (List U8) [TcpReadErr StreamErr, TcpUnexpectedEOF] -read_exactly! = |@Stream(stream), bytes_to_read| - Host.tcp_read_exactly!(stream, bytes_to_read) - |> Result.map_err( - |err| - if err == unexpected_eof_error_message then - TcpUnexpectedEOF - else - TcpReadErr(parse_stream_err(err)), - ) - -## Read until a delimiter or EOF is reached. -## -## ``` -## # Read until null terminator -## bytes = Tcp.read_until!(stream, 0)? -## ``` -## -## If found, the delimiter is included as the last byte. -## -## > To read until a newline is found, you can use [Tcp.read_line!] which -## conveniently decodes to a [Str]. -read_until! : Stream, U8 => Result (List U8) [TcpReadErr StreamErr] -read_until! = |@Stream(stream), byte| - Host.tcp_read_until!(stream, byte) - |> Result.map_err(|err| TcpReadErr(parse_stream_err(err))) - -## Read until a newline or EOF is reached. -## -## ``` -## # Read a line and then print it to `stdout` -## line_str = Tcp.read_line!(stream)? -## Stdout.line(line_str)? -## ``` -## -## If found, the newline is included as the last character in the [Str]. -## -read_line! : Stream => Result Str [TcpReadErr StreamErr, TcpReadBadUtf8 _] -read_line! = |stream| - bytes = read_until!(stream, '\n')? - - Str.from_utf8(bytes) - |> Result.map_err(TcpReadBadUtf8) - -## Writes bytes to a TCP stream. -## -## ``` -## # Writes the bytes 1, 2, 3 -## Tcp.write!(stream, [1, 2, 3])? -## ``` -## -## > To write a [Str], you can use [Tcp.write_utf8!] instead. -write! : Stream, List U8 => Result {} [TcpWriteErr StreamErr] -write! = |@Stream(stream), bytes| - Host.tcp_write!(stream, bytes) - |> Result.map_err(|err| TcpWriteErr(parse_stream_err(err))) - -## Writes a [Str] to a TCP stream, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). -## -## ``` -## # Write "Hi from Roc!" encoded as UTF-8 -## Tcp.write_utf8!(stream, "Hi from Roc!")? -## ``` -## -## > To write unformatted bytes, you can use [Tcp.write!] instead. -write_utf8! : Stream, Str => Result {} [TcpWriteErr StreamErr] -write_utf8! = |stream, str| - write!(stream, Str.to_utf8(str)) - -## Convert a [ConnectErr] to a [Str] you can print. -## -## ``` -## when err is -## TcpPerfomErr(TcpConnectErr(connect_err)) -> -## Stderr.line!(Tcp.connect_err_to_str(connect_err)) -## ``` -## -connect_err_to_str : (ConnectErr _) -> Str -connect_err_to_str = |err| - when err is - PermissionDenied -> "PermissionDenied" - AddrInUse -> "AddrInUse" - AddrNotAvailable -> "AddrNotAvailable" - ConnectionRefused -> "ConnectionRefused" - Interrupted -> "Interrupted" - TimedOut -> "TimedOut" - Unsupported -> "Unsupported" - Unrecognized(message) -> "Unrecognized Error: ${message}" - -## Convert a [StreamErr] to a [Str] you can print. -## -## ``` -## when err is -## TcpPerformErr(TcpReadErr(err)) -> -## err_str = Tcp.stream_err_to_str(err) -## Stderr.line!("Error while reading: ${err_str}") -## -## TcpPerformErr(TcpWriteErr(err)) -> -## err_str = Tcp.stream_err_to_str(err) -## Stderr.line!("Error while writing: ${err_str}") -## ``` -## -stream_err_to_str : StreamErr -> Str -stream_err_to_str = |err| - when err is - StreamNotFound -> "StreamNotFound" - PermissionDenied -> "PermissionDenied" - ConnectionRefused -> "ConnectionRefused" - ConnectionReset -> "ConnectionReset" - Interrupted -> "Interrupted" - OutOfMemory -> "OutOfMemory" - BrokenPipe -> "BrokenPipe" - Unrecognized(message) -> "Unrecognized Error: ${message}" + match err { + "StreamNotFound" => StreamNotFound + "ErrorKind::PermissionDenied" => PermissionDenied + "ErrorKind::ConnectionRefused" => ConnectionRefused + "ErrorKind::ConnectionReset" => ConnectionReset + "ErrorKind::Interrupted" => Interrupted + "ErrorKind::OutOfMemory" => OutOfMemory + "ErrorKind::BrokenPipe" => BrokenPipe + other => Unrecognized(other) + } diff --git a/platform/Tty.roc b/platform/Tty.roc index 8130730c..6b41a5b4 100644 --- a/platform/Tty.roc +++ b/platform/Tty.roc @@ -1,26 +1,19 @@ +import Host + ## 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 +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_mode_raw!({}) + ## 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 index 5af88029..c8a9c71b 100644 --- a/platform/Url.roc +++ b/platform/Url.roc @@ -1,523 +1,1391 @@ -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). +## A validated HTTP or HTTPS URL implemented entirely in Roc. ## -## 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] +## This is deliberately stricter than a browser parser. Hosts must be ASCII +## DNS names, dotted-decimal IPv4 addresses, or bracketed IPv6 addresses made +## from hexadecimal groups with optional :: elision. IPv4-in-IPv6 and Unicode +## domain names are intentionally unsupported. +Url :: { + scheme : [Http, Https], + host : Str, + port : [None, Some(U16)], + path : Str, + query : [None, Some(Str)], + fragment : [None, Some(Str)], +}.{ + + ## A reason a URL or relative reference could not be parsed. + ## + ## This error set describes this module's strict HTTP/HTTPS subset. Inputs + ## that browsers might repair, such as missing authority slashes or + ## backslashes, are rejected instead. + ParseErr : [ + CredentialsNotAllowed, + EmptyHost, + InternationalHostUnsupported, + InvalidCharacter(U8), + InvalidHost(Str), + InvalidIpv4(Str), + InvalidIpv6(Str), + InvalidPercentEncoding(U64), + InvalidPort(Str), + MissingAuthority, + MissingScheme, + PortOutOfRange(U64), + UnsupportedScheme(Str), + ] + + ## Parse a dynamic string as an absolute HTTP or HTTPS URL. + ## + ## The input must contain an explicit scheme and authority. Its host is + ## lowercased, default ports are removed, dot path segments are normalized, + ## and non-ASCII path, query, and fragment bytes are percent-encoded. + parse : Str -> Try(Url, ParseErr) + parse = |input| parse_absolute(input) + + ## Convert a quoted literal to a URL using the same validation as parse. + ## + ## Roc calls this automatically when a quoted literal is expected to have + ## type Url. A rejected literal reports a descriptive BadQuotedBytes error. + from_quote : Str -> Try(Url, [BadQuotedBytes(Str)]) + from_quote = |input| + match parse_absolute(input) { + Ok(url) => Ok(url) + Err(err) => Err(BadQuotedBytes(parse_err_to_str(err))) + } + + ## Parse a URL from a string value supplied by a generic encoding. + parser_for : encoding -> (state -> Try({ value : Url, rest : state }, err)) + where [ + encoding.parse_str : encoding, state -> Try({ value : Str, rest : state }, err), + encoding.invalid_value : encoding, state -> err, + ] + parser_for = |encoding| { + Encoding : encoding + + |state| { + parsed = Encoding.parse_str(encoding, state)? + + match parse(parsed.value) { + Ok(url) => Ok({ value: url, rest: parsed.rest }) + Err(_) => Err(Encoding.invalid_value(encoding, state)) + } + } + } + + ## Encode a URL as its canonical string through a generic encoding. + encoder_for : encoding -> (Url, state -> Try(state, err)) + where [ + encoding.encode_str : Str, state -> Try(state, err), + ] + encoder_for = |_encoding| { + Encoding : encoding + + |url, state| Encoding.encode_str(to_str(url), state) + } + + ## Serialize the URL in a stable normalized ASCII form. + ## + ## Scheme and DNS host names are lowercase, default ports are omitted, + ## paths are absolute, and IPv6 addresses use eight unpadded groups. + to_str : Url -> Str + to_str = |url| serialize(url, True) + + ## Render a URL for debugging and test failures. + to_inspect : Url -> Str + to_inspect = |url| "Url(${Json.to_str(to_str(url))})" + + ## Compare URLs by their canonical serialized representation. + is_eq : Url, Url -> Bool + is_eq = |left, right| Str.is_eq(to_str(left), to_str(right)) + + ## Hash URLs consistently with canonical equality. + to_hash : Url, Hasher -> Hasher + to_hash = |url, hasher| Str.to_hash(to_str(url), hasher) + + ## Return Http or Https. + scheme : Url -> [Http, Https] + scheme = |url| url.scheme + + ## Return the canonical host. + ## + ## IPv6 brackets are omitted; to_str includes them where required. + host : Url -> Str + host = |url| + if starts_with(url.host, "[") { + trim_brackets(url.host) + } else { + url.host + } + + ## Return an explicit non-default port. + ## + ## Ports 80 for HTTP and 443 for HTTPS are canonicalized to None. + port : Url -> [None, Some(U16)] + port = |url| url.port + + ## Return the absolute percent-encoded path, always beginning with slash. + path : Url -> Str + path = |url| url.path + + ## Return the percent-encoded query without its leading question mark. + ## + ## None and Some("") distinguish no query from a present empty query. + query : Url -> [None, Some(Str)] + query = |url| url.query + + ## Return the percent-encoded fragment without its leading hash. + ## + ## None and Some("") distinguish no fragment from a present empty fragment. + fragment : Url -> [None, Some(Str)] + fragment = |url| url.fragment + + ## Return this URL without its fragment. HTTP never transmits fragments. + without_fragment : Url -> Url + without_fragment = |url| + Url.{ + scheme: url.scheme, + host: url.host, + port: url.port, + path: url.path, + query: url.query, + fragment: None, + } + + ## Resolve a strict relative reference or absolute web URL against this URL. + ## + ## Root-relative, path-relative, query-only, and fragment-only references + ## are supported. Scheme-relative references are rejected. + resolve : Url, Str -> Try(Url, ParseErr) + resolve = |base, reference| resolve_reference(base, reference) + + ## Append unencoded path segments. + ## + ## Each list item is one segment, so slash characters inside an item are + ## percent-encoded rather than treated as separators. + append_path_segments : Url, List(Str) -> Url + append_path_segments = |url, segments| { + suffix = Str.join_with(segments.map(percent_encode), "/") + next_path = + if Str.is_empty(suffix) { + url.path + } else if url.path == "/" { + Str.concat("/", suffix) + } else if ends_with(url.path, "/") { + Str.concat(url.path, suffix) + } else { + Str.concat(Str.concat(url.path, "/"), suffix) + } + Url.{ + scheme: url.scheme, + host: url.host, + port: url.port, + path: normalize_path(next_path), + query: url.query, + fragment: url.fragment, + } + } + + ## Append one application/x-www-form-urlencoded query pair. + ## + ## Existing parameters, ordering, duplicate names, and the fragment are + ## preserved. + append_query_param : Url, Str, Str -> Url + append_query_param = |url, key, value| { + pair = Str.concat(Str.concat(form_encode(key), "="), form_encode(value)) + next_query = + match url.query { + None => pair + Some("") => pair + Some(existing) => Str.concat(Str.concat(existing, "&"), pair) + } + Url.{ + scheme: url.scheme, + host: url.host, + port: url.port, + path: url.path, + query: Some(next_query), + fragment: url.fragment, + } + } + + ## Decode the query into ordered name/value pairs. + ## + ## Plus signs decode as spaces, percent escapes decode as UTF-8 bytes, + ## parameters without equals receive an empty value, and duplicates remain. + query_pairs : Url -> List((Str, Str)) + query_pairs = |url| + match url.query { + None => [] + Some("") => [] + Some(query_str) => + Str.split_on(query_str, "&").map( + |pair| + match split_first(pair, "=") { + Found({ before, after }) => (form_decode(before), form_decode(after)) + NotFound => (form_decode(pair), "") + }, + ) + } + + ## Replace or remove the query. + ## + ## Some("") produces a present empty query. The supplied query may contain + ## Unicode but must otherwise already obey URL query syntax. + with_query : Url, [None, Some(Str)] -> Try(Url, ParseErr) + with_query = |url, option| { + next_query_option = + match option { + None => Ok(None) + Some(raw) => + match validate_component(raw, Query) { + Ok(value) => Ok(Some(value)) + Err(err) => Err(err) + } + }? + Ok( + Url.{ + scheme: url.scheme, + host: url.host, + port: url.port, + path: url.path, + query: next_query_option, + fragment: url.fragment, + }, + ) + } + + ## Replace or remove the fragment. + ## + ## Some("") produces a present empty fragment. Unicode is percent-encoded. + with_fragment : Url, [None, Some(Str)] -> Try(Url, ParseErr) + with_fragment = |url, option| { + next_fragment_option = + match option { + None => Ok(None) + Some(raw) => + match validate_component(raw, Fragment) { + Ok(value) => Ok(Some(value)) + Err(err) => Err(err) + } + }? + Ok( + Url.{ + scheme: url.scheme, + host: url.host, + port: url.port, + path: url.path, + query: url.query, + fragment: next_fragment_option, + }, + ) + } +} + +# Absolute URL and authority parsing. + +parse_absolute : Str -> Try(Url, Url.ParseErr) +parse_absolute = |input| { + scheme_parts = + match split_first(input, "://") { + Found(parts) => Ok(parts) + NotFound => + if Str.contains(input, ":") { + Err(MissingAuthority) + } else { + Err(MissingScheme) + } + }? + scheme = + match ascii_lower(scheme_parts.before) { + "http" => Ok(Http) + "https" => Ok(Https) + other => Err(UnsupportedScheme(other)) + }? + { authority, suffix } = split_authority(scheme_parts.after) + if Str.is_empty(authority) { + Err(EmptyHost) + } else if Str.contains(authority, "@") { + Err(CredentialsNotAllowed) + } else { + parsed_authority = parse_authority(authority, scheme)? + components = parse_suffix(suffix)? + Ok( + Url.{ + scheme, + host: parsed_authority.host, + port: parsed_authority.port, + path: normalize_path(components.path), + query: components.query, + fragment: components.fragment, + }, + ) + } +} + +parse_authority : Str, [Http, Https] -> Try({ host : Str, port : [None, Some(U16)] }, Url.ParseErr) +parse_authority = |authority, scheme| { + if starts_with(authority, "[") { + match split_first(authority, "]") { + NotFound => Err(InvalidIpv6(authority)) + Found({ before, after }) => { + raw_ipv6 = drop_prefix(before, "[") + host = validate_ipv6(raw_ipv6)? + port = + if Str.is_empty(after) { + Ok(None) + } else if starts_with(after, ":") { + parse_port(drop_prefix(after, ":"), scheme) + } else { + Err(InvalidIpv6(authority)) + } + Ok({ host: Str.concat(Str.concat("[", host), "]"), port: port? }) + } + } + } else { + { raw_host, raw_port } = + match split_last(authority, ":") { + Found({ before, after }) => { raw_host: before, raw_port: Some(after) } + NotFound => { raw_host: authority, raw_port: None } + } + host = validate_host(raw_host)? + port = + match raw_port { + None => Ok(None) + Some(raw) => parse_port(raw, scheme) + } + Ok({ host, port: port? }) + } +} + +validate_host : Str -> Try(Str, Url.ParseErr) +validate_host = |raw_host| { + if Str.is_empty(raw_host) { + Err(EmptyHost) + } else if List.any(Str.to_utf8(raw_host), |byte| byte > 127) { + Err(InternationalHostUnsupported) + } else if List.all(Str.to_utf8(raw_host), |byte| is_digit(byte) or byte == 46) { + validate_ipv4(raw_host) + } else { + validate_dns_name(raw_host) + } +} + +validate_dns_name : Str -> Try(Str, [InvalidHost(Str), ..]) +validate_dns_name = |raw_host| { + host = ascii_lower(raw_host) + labels = Str.split_on(host, ".") + valid = + List.len(Str.to_utf8(host)) <= 253 and + List.all( + labels, + |label| { + bytes = Str.to_utf8(label) + len = List.len(bytes) + len > 0 and len <= 63 and + is_alphanumeric(first_or_zero(bytes)) and + is_alphanumeric(last_or_zero(bytes)) and + List.all(bytes, |byte| is_alphanumeric(byte) or byte == 45) + }, + ) + if valid { + Ok(host) + } else { + Err(InvalidHost(raw_host)) + } +} + +validate_ipv4 : Str -> Try(Str, [InvalidIpv4(Str), ..]) +validate_ipv4 = |raw_host| { + parts = Str.split_on(raw_host, ".") + if List.len(parts) != 4 { + Err(InvalidIpv4(raw_host)) + } else { + match parse_ipv4_parts(parts, []) { + Err(_) => Err(InvalidIpv4(raw_host)) + Ok(values) => Ok(Str.join_with(values.map(U64.to_str), ".")) + } + } +} + +parse_ipv4_parts : List(Str), List(U64) -> Try(List(U64), [BadIpv4Part]) +parse_ipv4_parts = |parts, out| + match parts { + [] => Ok(out) + [first, .. as rest] => + match parse_decimal(first) { + Ok(value) => + if value <= 255 { + parse_ipv4_parts(rest, out.append(value)) + } else { + Err(BadIpv4Part) + } + Err(_) => Err(BadIpv4Part) + } + } + +parse_port : Str, [Http, Https] -> Try([None, Some(U16)], [InvalidPort(Str), PortOutOfRange(U64), ..]) +parse_port = |raw, scheme| + match parse_decimal(raw) { + Err(_) => Err(InvalidPort(raw)) + Ok(value) => + if value > 65535 { + Err(PortOutOfRange(value)) + } else { + port = U64.to_u16_wrap(value) + is_default = + match scheme { + Http => port == 80 + Https => port == 443 + } + Ok( + if is_default { + None + } else { + Some(port) + }, + ) + } + } + +parse_decimal : Str -> Try(U64, [NotDecimal]) +parse_decimal = |raw| { + bytes = Str.to_utf8(raw) + if List.is_empty(bytes) or Bool.not(List.all(bytes, is_digit)) { + Err(NotDecimal) + } else { + Ok(List.fold(bytes, 0, |acc, byte| acc * 10 + U8.to_u64(byte - 48))) + } +} + +# IPv6 parsing. +# +# Addresses are validated as eight hexadecimal groups, with at most one :: +# elision. IPv4-in-IPv6 syntax is outside this module's deliberately small +# subset. Serialization expands elided groups and removes leading zeroes. +validate_ipv6 : Str -> Try(Str, [InvalidIpv6(Str), ..]) +validate_ipv6 = |raw| { + pieces = Str.split_on(raw, "::") + if List.len(pieces) > 2 { + Err(InvalidIpv6(raw)) + } else if List.len(pieces) == 1 { + groups = parse_ipv6_side(raw)? + if List.len(groups) == 8 { + Ok(serialize_ipv6(groups)) + } else { + Err(InvalidIpv6(raw)) + } + } else { + left = parse_ipv6_side(get_or_empty(pieces, 0))? + right = parse_ipv6_side(get_or_empty(pieces, 1))? + count = List.len(left) + List.len(right) + if count >= 8 { + Err(InvalidIpv6(raw)) + } else { + groups = List.concat(List.concat(left, List.repeat(0, 8 - count)), right) + Ok(serialize_ipv6(groups)) + } + } +} + +parse_ipv6_side : Str -> Try(List(U16), [InvalidIpv6(Str), ..]) +parse_ipv6_side = |raw| + if Str.is_empty(raw) { + Ok([]) + } else { + parse_hex_groups(Str.split_on(raw, ":"), []) + } + +parse_hex_groups : List(Str), List(U16) -> Try(List(U16), [InvalidIpv6(Str), ..]) +parse_hex_groups = |parts, out| + match parts { + [] => Ok(out) + [first, .. as rest] => { + bytes = Str.to_utf8(first) + if List.is_empty(bytes) or List.len(bytes) > 4 or Bool.not(List.all(bytes, is_hex)) { + Err(InvalidIpv6(first)) + } else { + value = List.fold(bytes, 0, |acc, byte| acc * 16 + U8.to_u16(hex_value(byte))) + parse_hex_groups(rest, out.append(value)) + } + } + } + +serialize_ipv6 : List(U16) -> Str +serialize_ipv6 = |groups| Str.join_with(groups.map(u16_to_hex), ":") + +u16_to_hex : U16 -> Str +u16_to_hex = |value| + if value == 0 { + "0" + } else { + u16_to_hex_help(value, []) + } + +u16_to_hex_help : U16, List(U8) -> Str +u16_to_hex_help = |value, digits| { + next_digits = [lower_hex_digit_byte(U16.to_u8_wrap(value % 16))].concat(digits) + next = value // 16 + if next == 0 { + Str.from_utf8_lossy(next_digits) + } else { + u16_to_hex_help(next, next_digits) + } +} + +parse_suffix : Str -> Try({ fragment : [None, Some(Str)], path : Str, query : [None, Some(Str)] }, Url.ParseErr) +parse_suffix = |suffix| { + { before_fragment, fragment } = + match split_first(suffix, "#") { + Found({ before, after }) => { before_fragment: before, fragment: Some(after) } + NotFound => { before_fragment: suffix, fragment: None } + } + { raw_path, query } = + match split_first(before_fragment, "?") { + Found({ before, after }) => { raw_path: before, query: Some(after) } + NotFound => { raw_path: before_fragment, query: None } + } + path_input = if Str.is_empty(raw_path) { + "/" + } else { + raw_path + } + if Bool.not(starts_with(path_input, "/")) { + Err(InvalidCharacter(first_or_zero(Str.to_utf8(path_input)))) + } else { + path = validate_component(path_input, Path)? + encoded_query = validate_optional(query, Query)? + encoded_fragment = validate_optional(fragment, Fragment)? + Ok({ path, query: encoded_query, fragment: encoded_fragment }) + } +} + +validate_optional : [None, Some(Str)], [Fragment, Path, Query] -> Try([None, Some(Str)], Url.ParseErr) +validate_optional = |option, kind| + match option { + None => Ok(None) + Some(raw) => + match validate_component(raw, kind) { + Ok(value) => Ok(Some(value)) + Err(err) => Err(err) + } + } + +validate_component : Str, [Fragment, Path, Query] -> Try(Str, Url.ParseErr) +validate_component = |raw, kind| + match validate_component_help(Str.to_utf8(raw), kind, 0, []) { + Ok(bytes) => Ok(Str.from_utf8_lossy(bytes)) + Err(err) => Err(err) + } + +validate_component_help : List(U8), [Fragment, Path, Query], U64, List(U8) -> Try(List(U8), Url.ParseErr) +validate_component_help = |bytes, kind, index, out| { + if index >= List.len(bytes) { + Ok(out) + } else { + byte = get_or_zero(bytes, index) + if byte == 37 { + if index + 2 >= List.len(bytes) or Bool.not(is_hex(get_or_zero(bytes, index + 1))) or Bool.not(is_hex(get_or_zero(bytes, index + 2))) { + Err(InvalidPercentEncoding(index)) + } else { + next = out.append(37) + .append(ascii_upper_hex(get_or_zero(bytes, index + 1))) + .append(ascii_upper_hex(get_or_zero(bytes, index + 2))) + validate_component_help(bytes, kind, index + 3, next) + } + } else if byte > 127 { + validate_component_help(bytes, kind, index + 1, append_percent_byte(out, byte)) + } else if is_forbidden(byte, kind) { + Err(InvalidCharacter(byte)) + } else { + validate_component_help(bytes, kind, index + 1, out.append(byte)) + } + } +} + +is_forbidden : U8, [Fragment, Path, Query] -> Bool +is_forbidden = |byte, kind| { + common = byte <= 32 or byte == 127 or byte == 34 or byte == 60 or byte == 62 or byte == 92 + if common { + True + } else { + match kind { + Path => byte == 35 or byte == 63 + Query => byte == 35 + Fragment => False + } + } +} + +# Relative-reference resolution and path normalization. + +resolve_reference : Url, Str -> Try(Url, Url.ParseErr) +resolve_reference = |base, reference| { + lower = ascii_lower(reference) + if starts_with(lower, "http://") or starts_with(lower, "https://") { + parse_absolute(reference) + } else if Str.contains(reference, "://") or starts_with(reference, "//") { + Err(MissingScheme) + } else { + relative = parse_relative(reference)? + next_path = + if Str.is_empty(relative.path) { + base.path + } else if starts_with(relative.path, "/") { + normalize_path(relative.path) + } else { + normalize_path(Str.concat(path_directory(base.path), relative.path)) + } + next_query = + match relative.query { + Some(value) => Some(value) + None => if Str.is_empty(relative.path) { + base.query + } else { + None + } + } + Ok( + Url.{ + scheme: base.scheme, + host: base.host, + port: base.port, + path: next_path, + query: next_query, + fragment: relative.fragment, + }, + ) + } +} + +parse_relative : Str -> Try({ fragment : [None, Some(Str)], path : Str, query : [None, Some(Str)] }, Url.ParseErr) +parse_relative = |reference| { + { before_fragment, fragment } = + match split_first(reference, "#") { + Found({ before, after }) => { before_fragment: before, fragment: Some(after) } + NotFound => { before_fragment: reference, fragment: None } + } + { raw_path, query } = + match split_first(before_fragment, "?") { + Found({ before, after }) => { raw_path: before, query: Some(after) } + NotFound => { raw_path: before_fragment, query: None } + } + path = validate_component(raw_path, Path)? + encoded_query = validate_optional(query, Query)? + encoded_fragment = validate_optional(fragment, Fragment)? + Ok({ path, query: encoded_query, fragment: encoded_fragment }) +} + +normalize_path : Str -> Str +normalize_path = |path_str| { + rooted = if starts_with(path_str, "/") { + path_str + } else { + Str.concat("/", path_str) + } + trailing = ends_with(rooted, "/") or ends_with(rooted, "/.") or ends_with(rooted, "/..") + normalized = normalize_segments(Str.split_on(rooted, "/"), []) + joined = Str.concat("/", Str.join_with(normalized, "/")) + if trailing and joined != "/" { + Str.concat(joined, "/") + } else { + joined + } +} + +normalize_segments : List(Str), List(Str) -> List(Str) +normalize_segments = |segments, out| + match segments { + [] => out + ["", .. as rest] => normalize_segments(rest, out) + [".", .. as rest] => normalize_segments(rest, out) + ["..", .. as rest] => normalize_segments(rest, List.drop_last(out, 1)) + [first, .. as rest] => normalize_segments(rest, out.append(first)) + } + +path_directory : Str -> Str +path_directory = |path_str| { + parts = Str.split_on(path_str, "/") + if List.len(parts) <= 2 { + "/" + } else { + Str.concat(Str.join_with(List.drop_last(parts, 1), "/"), "/") + } +} + +serialize : Url, Bool -> Str +serialize = |url, include_fragment| { + scheme_str = + match url.scheme { + Http => "http" + Https => "https" + } + port_str = + match url.port { + None => "" + Some(value) => Str.concat(":", U16.to_str(value)) + } + query_str = + match url.query { + None => "" + Some(value) => Str.concat("?", value) + } + fragment_str = + if include_fragment { + match url.fragment { + None => "" + Some(value) => Str.concat("#", value) + } + } else { + "" + } + Str.concat( + Str.concat( + Str.concat( + Str.concat( + Str.concat(Str.concat(scheme_str, "://"), url.host), + port_str, + ), + url.path, + ), + query_str, + ), + fragment_str, + ) +} + +# Percent encoding and application/x-www-form-urlencoded query handling. -## 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, "?") + Str.from_utf8_lossy( + List.fold( + Str.to_utf8(input), + [], + |out, byte| + if is_unreserved(byte) { + out.append(byte) + } else { + append_percent_byte(out, byte) + }, + ), + ) -## 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) -> "" +form_encode : Str -> Str +form_encode = |input| + Str.from_utf8_lossy( + List.fold( + Str.to_utf8(input), + [], + |out, byte| + if byte == 32 { + out.append(43) + } else if is_form_unescaped(byte) { + out.append(byte) + } else { + append_percent_byte(out, byte) + }, + ), + ) -## 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, "#") +form_decode : Str -> Str +form_decode = |input| Str.from_utf8_lossy(form_decode_help(Str.to_utf8(input), 0, [])) -# 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 +form_decode_help : List(U8), U64, List(U8) -> List(U8) +form_decode_help = |bytes, index, out| { + if index >= List.len(bytes) { + out + } else { + byte = get_or_zero(bytes, index) + if byte == 43 { + form_decode_help(bytes, index + 1, out.append(32)) + } else if byte == 37 and index + 2 < List.len(bytes) and is_hex(get_or_zero(bytes, index + 1)) and is_hex(get_or_zero(bytes, index + 2)) { + decoded = hex_value(get_or_zero(bytes, index + 1)) * 16 + hex_value(get_or_zero(bytes, index + 2)) + form_decode_help(bytes, index + 3, out.append(decoded)) + } else { + form_decode_help(bytes, index + 1, out.append(byte)) + } + } +} + +# Small string and byte helpers. Keeping these local avoids depending on host +# code or exposing parser implementation details through the public API. + +split_authority : Str -> { authority : Str, suffix : Str } +split_authority = |after_scheme| { + bytes = Str.to_utf8(after_scheme) + index = first_delimiter(bytes, 0) + authority = Str.from_utf8_lossy(List.sublist(bytes, { start: 0, len: index })) + suffix = Str.from_utf8_lossy(List.sublist(bytes, { start: index, len: List.len(bytes) - index })) + { authority, suffix } +} + +first_delimiter : List(U8), U64 -> U64 +first_delimiter = |bytes, index| { + if index >= List.len(bytes) { + index + } else { + byte = get_or_zero(bytes, index) + if byte == 47 or byte == 63 or byte == 35 { + index + } else { + first_delimiter(bytes, index + 1) + } + } +} + +parse_err_to_str : Url.ParseErr -> Str +parse_err_to_str = |err| + match err { + CredentialsNotAllowed => "URL credentials are not supported" + EmptyHost => "URL host is empty" + InternationalHostUnsupported => "URL host must be ASCII; use its Punycode form" + InvalidCharacter(byte) => Str.concat("URL contains invalid byte ", U8.to_str(byte)) + InvalidHost(host) => Str.concat("Invalid URL host: ", host) + InvalidIpv4(host) => Str.concat("Invalid IPv4 address: ", host) + InvalidIpv6(host) => Str.concat("Invalid IPv6 address: ", host) + InvalidPercentEncoding(index) => Str.concat("Invalid percent escape at byte ", U64.to_str(index)) + InvalidPort(port) => Str.concat("Invalid URL port: ", port) + MissingAuthority => "URL must contain :// after its scheme" + MissingScheme => "URL must start with http:// or https://" + PortOutOfRange(port) => Str.concat("URL port is out of range: ", U64.to_str(port)) + UnsupportedScheme(scheme) => Str.concat("Unsupported URL scheme: ", scheme) + } + +ascii_lower : Str -> Str +ascii_lower = |input| + Str.from_utf8_lossy( + Str.to_utf8(input).map( + |byte| + if byte >= 65 and byte <= 90 { + byte + 32 + } else { + byte + }, + ), + ) + +ascii_upper_hex : U8 -> U8 +ascii_upper_hex = |byte| + if byte >= 97 and byte <= 102 { + byte - 32 + } else { + byte + } + +is_unreserved : U8 -> Bool +is_unreserved = |byte| is_alphanumeric(byte) or byte == 45 or byte == 46 or byte == 95 or byte == 126 + +is_form_unescaped : U8 -> Bool +is_form_unescaped = |byte| is_alphanumeric(byte) or byte == 42 or byte == 45 or byte == 46 or byte == 95 + +is_alphanumeric : U8 -> Bool +is_alphanumeric = |byte| is_digit(byte) or (byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122) + +is_digit : U8 -> Bool +is_digit = |byte| byte >= 48 and byte <= 57 + +is_hex : U8 -> Bool +is_hex = |byte| is_digit(byte) or (byte >= 65 and byte <= 70) or (byte >= 97 and byte <= 102) + +hex_value : U8 -> U8 +hex_value = |byte| + if byte <= 57 { + byte - 48 + } else if byte <= 70 { + byte - 55 + } else { + byte - 87 + } + +append_percent_byte : List(U8), U8 -> List(U8) +append_percent_byte = |out, byte| + out.append(37).append(hex_digit_byte(byte // 16)).append(hex_digit_byte(byte % 16)) + +hex_digit_byte : U8 -> U8 +hex_digit_byte = |value| + if value < 10 { + value + 48 + } else { + value + 55 + } + +lower_hex_digit_byte : U8 -> U8 +lower_hex_digit_byte = |value| + if value < 10 { + value + 48 + } else { + value + 87 + } + +get_or_zero : List(U8), U64 -> U8 +get_or_zero = |list, index| + match list.get(index) { + Ok(value) => value + Err(_) => 0 + } + +get_or_empty : List(Str), U64 -> Str +get_or_empty = |list, index| + match list.get(index) { + Ok(value) => value + Err(_) => "" + } + +first_or_zero : List(U8) -> U8 +first_or_zero = |list| get_or_zero(list, 0) + +last_or_zero : List(U8) -> U8 +last_or_zero = |list| + if List.is_empty(list) { + 0 + } else { + get_or_zero(list, List.len(list) - 1) + } + +trim_brackets : Str -> Str +trim_brackets = |str| { + bytes = Str.to_utf8(str) + if List.len(bytes) < 2 { + "" + } else { + Str.from_utf8_lossy(List.sublist(bytes, { start: 1, len: List.len(bytes) - 2 })) + } +} + +starts_with : Str, Str -> Bool +starts_with = |str, prefix| + if Str.is_empty(prefix) { + True + } else { + match split_first(str, prefix) { + Found({ before, after: _ }) => Str.is_empty(before) + NotFound => False + } + } + +ends_with : Str, Str -> Bool +ends_with = |str, suffix| { + if Str.is_empty(suffix) { + True + } else { + parts = Str.split_on(str, suffix) + match parts.get(List.len(parts) - 1) { + Ok(last) => Str.is_empty(last) + Err(_) => False + } + } +} + +drop_prefix : Str, Str -> Str +drop_prefix = |str, prefix| { + parts = Str.split_on(str, prefix) + Str.join_with(List.drop_first(parts, 1), prefix) +} + +split_first : Str, Str -> [Found({ after : Str, before : Str }), NotFound] +split_first = |str, separator| { + parts = Str.split_on(str, separator) + if List.len(parts) > 1 { + match parts.get(0) { + Ok(before) => Found({ before, after: Str.join_with(List.drop_first(parts, 1), separator) }) + Err(_) => NotFound + } + } else { + NotFound + } +} + +split_last : Str, Str -> [Found({ after : Str, before : Str }), NotFound] +split_last = |str, separator| { + parts = Str.split_on(str, separator) + if List.len(parts) > 1 { + match parts.get(List.len(parts) - 1) { + Ok(after) => Found({ before: Str.join_with(List.drop_last(parts, 1), separator), after }) + Err(_) => NotFound + } + } else { + NotFound + } +} + +# The parsing cases below are a curated strict HTTP/HTTPS subset informed by +# web-platform-tests/url/resources/urltestdata.json at WPT commit +# dc97e7bed3096ac9e0e591ab5fa22e7fb8844ead (BSD-3-Clause). + +expect + match Url.parse("HTTP://Example.COM:80/a/../b") { + Ok(url) => Url.to_str(url) == "http://example.com/b" + Err(_) => False + } + +expect + match Url.parse("https://example.com:443") { + Ok(url) => Url.scheme(url) == Https and Url.host(url) == "example.com" and Url.port(url) == None and Url.path(url) == "/" + Err(_) => False + } + +expect + match Url.parse("https://127.000.000.001:8443/") { + Ok(url) => Url.to_str(url) == "https://127.0.0.1:8443/" + Err(_) => False + } + +expect + match Url.parse("http://[::1]:8080/") { + Ok(url) => Url.host(url) == "0:0:0:0:0:0:0:1" and Url.to_str(url) == "http://[0:0:0:0:0:0:0:1]:8080/" + Err(_) => False + } + +expect + match Url.parse("https://example.com/café?q=naïve#résumé") { + Ok(url) => Url.to_str(url) == "https://example.com/caf%C3%A9?q=na%C3%AFve#r%C3%A9sum%C3%A9" + Err(_) => False + } + +expect + match Url.parse("https://example.com/%7euser") { + Ok(url) => Url.to_str(url) == "https://example.com/%7Euser" + Err(_) => False + } + +expect Url.parse("example.com") == Err(MissingScheme) + +expect Url.parse("mailto:user@example.com") == Err(MissingAuthority) + +expect Url.parse("ftp://example.com") == Err(UnsupportedScheme("ftp")) + +expect Url.parse("https://user:secret@example.com") == Err(CredentialsNotAllowed) + +expect Url.parse("https://münich.example") == Err(InternationalHostUnsupported) + +expect Url.parse("https://") == Err(EmptyHost) + +expect Url.parse("https://-example.com") == Err(InvalidHost("-example.com")) + +expect Url.parse("https://example..com") == Err(InvalidHost("example..com")) + +expect Url.parse("https://127.0.0.256") == Err(InvalidIpv4("127.0.0.256")) + +expect Url.parse("https://127.0.0") == Err(InvalidIpv4("127.0.0")) + +expect + match Url.parse("https://[:::1]") { + Err(InvalidIpv6(_)) => True + _ => False + } + +expect Url.parse("https://example.com:wat") == Err(InvalidPort("wat")) + +expect Url.parse("https://example.com:70000") == Err(PortOutOfRange(70000)) + +expect Url.parse("https://example.com/%zz") == Err(InvalidPercentEncoding(1)) + +expect Url.parse("https://example.com/a b") == Err(InvalidCharacter(32)) + +expect Url.parse("https://example.com/a\\b") == Err(InvalidCharacter(92)) + +expect + match Url.parse("https://example.com/?#") { + Ok(url) => Url.query(url) == Some("") and Url.fragment(url) == Some("") + Err(_) => False + } + +expect + match Url.parse("https://example.com/") { + Err(_) => False + Ok(url) => { + with_path = Url.append_path_segments(url, ["a/b", "café"]) + with_first = Url.append_query_param(with_path, "tag", "one") + built = Url.append_query_param(with_first, "tag", "two words") + Url.to_str(built) == "https://example.com/a%2Fb/caf%C3%A9?tag=one&tag=two+words" and + Url.query_pairs(built) == [("tag", "one"), ("tag", "two words")] + } + } + +expect + match Url.parse("https://example.com/a/b?old=1#old") { + Err(_) => False + Ok(base) => + match Url.resolve(base, "../c?new=2#fresh") { + Ok(resolved) => Url.to_str(resolved) == "https://example.com/c?new=2#fresh" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/a/b?old=1#old") { + Err(_) => False + Ok(base) => + match Url.resolve(base, "?new=2") { + Ok(resolved) => Url.to_str(resolved) == "https://example.com/a/b?new=2" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/a/b?old=1") { + Err(_) => False + Ok(base) => + match Url.resolve(base, "#fresh") { + Ok(resolved) => Url.to_str(resolved) == "https://example.com/a/b?old=1#fresh" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/a/b") { + Err(_) => False + Ok(base) => + match Url.resolve(base, "/root/./x/../y") { + Ok(resolved) => Url.to_str(resolved) == "https://example.com/root/y" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/a?x=1#frag") { + Err(_) => False + Ok(url) => Url.to_str(Url.without_fragment(url)) == "https://example.com/a?x=1" + } + +expect + match Url.from_quote("https://example.com") { + Ok(url) => Url.to_str(url) == "https://example.com/" + Err(_) => False + } + +expect + match Url.parse("http://localhost:0/a/./b/../../c/") { + Ok(url) => Url.port(url) == Some(0) and Url.to_str(url) == "http://localhost:0/c/" + Err(_) => False + } + +expect + match Url.parse("https://example.com:65535") { + Ok(url) => Url.port(url) == Some(65535) and Url.to_str(url) == "https://example.com:65535/" + Err(_) => False + } + +expect Url.parse("https://example.com:") == Err(InvalidPort("")) + +expect Url.parse("https://example.com:65536") == Err(PortOutOfRange(65536)) + +expect Url.parse("https://example_com") == Err(InvalidHost("example_com")) + +expect Url.parse("https://example.com.") == Err(InvalidHost("example.com.")) + +expect + match Url.parse("http://[2001:0DB8:0000:0000:0000:ff00:0042:8329]/") { + Ok(url) => Url.host(url) == "2001:db8:0:0:0:ff00:42:8329" and Url.to_str(url) == "http://[2001:db8:0:0:0:ff00:42:8329]/" + Err(_) => False + } + +expect + match Url.parse("http://[::]/") { + Ok(url) => Url.host(url) == "0:0:0:0:0:0:0:0" + Err(_) => False + } + +expect + match Url.parse("http://[1:2:3:4:5:6:7]/") { + Err(InvalidIpv6(_)) => True + _ => False + } + +expect + match Url.parse("http://[1:2:3:4:5:6:7:8:9]/") { + Err(InvalidIpv6(_)) => True + _ => False + } + +expect + match Url.parse("http://[::ffff:192.0.2.1]/") { + Err(InvalidIpv6(_)) => True + _ => False + } + +expect + match Url.parse("https://example.com/a/%2f/%aa") { + Ok(url) => Url.path(url) == "/a/%2F/%AA" + Err(_) => False + } + +expect Url.parse("https://example.com/%") == Err(InvalidPercentEncoding(1)) + +expect Url.parse("https://example.com/%0") == Err(InvalidPercentEncoding(1)) + +expect Url.parse("https://example.com/") == Err(InvalidCharacter(60)) + +expect + match Url.parse("https://example.com/path?reserved=%23%26/?#fragment/?") { + Ok(url) => + Url.path(url) == "/path" and + Url.query(url) == Some("reserved=%23%26/?") and + Url.fragment(url) == Some("fragment/?") + Err(_) => False + } + +expect + match Url.parse("https://example.com/?name=Roc+Lang&letter=%C3%A9&flag&name=again") { + Ok(url) => Url.query_pairs(url) == [("name", "Roc Lang"), ("letter", "é"), ("flag", ""), ("name", "again")] + Err(_) => False + } + +expect + match Url.parse("https://example.com/?") { + Ok(url) => Url.query_pairs(url) == [] + Err(_) => False + } + +expect + match Url.parse("https://example.com/base?old=1#frag") { + Err(_) => False + Ok(url) => { + appended = Url.append_path_segments(url, ["space here", "?and#"]) + Url.to_str(appended) == "https://example.com/base/space%20here/%3Fand%23?old=1#frag" + } + } + +expect + match Url.parse("https://example.com/base") { + Err(_) => False + Ok(url) => Url.append_path_segments(url, []) == url + } + +expect + match Url.parse("https://example.com/path?old=1#frag") { + Err(_) => False + Ok(url) => + match Url.with_query(url, None) { + Ok(changed) => Url.to_str(changed) == "https://example.com/path#frag" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/path") { + Err(_) => False + Ok(url) => + match Url.with_query(url, Some("term=café&empty=")) { + Ok(changed) => Url.to_str(changed) == "https://example.com/path?term=caf%C3%A9&empty=" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/path") { + Err(_) => False + Ok(url) => Url.with_query(url, Some("bad#query")) == Err(InvalidCharacter(35)) + } + +expect + match Url.parse("https://example.com/path#old") { + Err(_) => False + Ok(url) => + match Url.with_fragment(url, None) { + Ok(changed) => Url.to_str(changed) == "https://example.com/path" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/path") { + Err(_) => False + Ok(url) => + match Url.with_fragment(url, Some("résumé/?")) { + Ok(changed) => Url.to_str(changed) == "https://example.com/path#r%C3%A9sum%C3%A9/?" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/path") { + Err(_) => False + Ok(url) => Url.with_fragment(url, Some("bad\\fragment")) == Err(InvalidCharacter(92)) + } + +expect + match Url.parse("https://example.com/a/b?old=1#old") { + Err(_) => False + Ok(base) => + match Url.resolve(base, "") { + Ok(resolved) => Url.to_str(resolved) == "https://example.com/a/b?old=1" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/a/b") { + Err(_) => False + Ok(base) => + match Url.resolve(base, "../../../root") { + Ok(resolved) => Url.to_str(resolved) == "https://example.com/root" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/a/b") { + Err(_) => False + Ok(base) => + match Url.resolve(base, "HTTP://Other.EXAMPLE:80/x") { + Ok(resolved) => Url.to_str(resolved) == "http://other.example/x" + Err(_) => False + } + } + +expect + match Url.parse("https://example.com/a/b") { + Err(_) => False + Ok(base) => Url.resolve(base, "//other.example/x") == Err(MissingScheme) + } + +expect + match Url.parse("https://example.com/a/b") { + Err(_) => False + Ok(base) => Url.resolve(base, "ftp://other.example/x") == Err(MissingScheme) + } + +expect + match Url.from_quote("not a url") { + Err(BadQuotedBytes(message)) => Str.contains(message, "http:// or https://") + Ok(_) => False + } + +## Inspection uses the canonical URL and identifies the nominal type. +expect + match Url.parse("HTTPS://EXAMPLE.COM:443/a") { + Ok(url) => Str.inspect(url) == "Url(\"https://example.com/a\")" + Err(_) => False + } + +## Canonically equivalent URLs compare and hash identically. +expect + match (Url.parse("HTTPS://EXAMPLE.COM:443/a"), Url.parse("https://example.com/a")) { + (Ok(stored), Ok(lookup)) => stored == lookup and Dict.single(stored, "found").get(lookup) == Ok("found") + _ => False + } + +## Generic encoders represent URLs as canonical strings. +expect { + url : Url + url = "https://example.com/a?q=roc" + Json.to_str(url) == "\"https://example.com/a?q=roc\"" +} + +## Generic parsers validate and canonicalize encoded URL strings. +expect { + decoded : Try(Url, Json.ParseErr) + decoded = Json.parse("\"HTTPS://EXAMPLE.COM:443/a\"") + + match decoded { + Ok(url) => Url.to_str(url) == "https://example.com/a" + Err(_) => False + } +} + +expect { + decoded : Try(Url, Json.ParseErr) + decoded = Json.parse("\"not a url\"") + decoded == Err(Json.invalid_json) +} diff --git a/platform/Utc.roc b/platform/Utc.roc index 68ba767b..30e64092 100644 --- a/platform/Utc.roc +++ b/platform/Utc.roc @@ -1,79 +1,54 @@ -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 +## Read UTC time and convert Unix timestamps into common representations. +Utc :: [].{ + + ## Get the current UTC time as nanoseconds since the Unix epoch (January 1, 1970). + now! : () => U128 + now! = || Host.utc_now!() ?? 0 + + ## 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 + + ## Convert a timestamp to nanoseconds since epoch. + to_nanos_since_epoch : U128 -> U128 + to_nanos_since_epoch = |nanos| nanos + + ## Convert nanoseconds since epoch to a timestamp. + from_nanos_since_epoch : U128 -> U128 + from_nanos_since_epoch = |nanos| nanos + + ## 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 + } + + ## Convert a timestamp to an ISO 8601 UTC string with second precision. + to_iso_8601 : U128 -> Str + to_iso_8601 = |nanos| { + millis = to_millis_since_epoch(nanos) + datetime = InternalDateTime.epoch_millis_to_datetime(millis) + + InternalDateTime.to_iso_8601(datetime) + } +} 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..c2237ee6 100644 --- a/platform/main.roc +++ b/platform/main.roc @@ -1,69 +1,123 @@ -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!] +## A native command-line platform with filesystem, process, network, terminal, +## SQLite, environment, random, and UTC effects. +platform "" + requires { + main! : List([Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))]) => Try({}, [Exit(I32), ..]) + } + exposes [Cmd, Env, File, Http, IOErr, Locale, OsStr, Path, Random, Sleep, Sqlite, Stdin, Stdout, Stderr, Tcp, Tty, Url, Utc] + packages { + # HTTP data types (Method, Request, Response) come from the shared + # roc-lang/http package so apps and other packages using it see the same + # 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", + } + 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!, + # New file hosted functions are kept at the end so adding them does not + # renumber the generated glue types for existing modules. + "hosted_file_hard_link": Host.file_hard_link!, + "hosted_file_rename": Host.file_rename!, + # SQLite hosted functions are kept at the end so adding them does not + # renumber the generated glue types for the modules declared above. + "hosted_sqlite_prepare": Host.sqlite_prepare!, + "hosted_sqlite_bind": Host.sqlite_bind!, + "hosted_sqlite_columns": Host.sqlite_columns!, + "hosted_sqlite_column_value": Host.sqlite_column_value!, + "hosted_sqlite_step": Host.sqlite_step!, + "hosted_sqlite_reset": Host.sqlite_reset!, + # 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!, + # Environment additions are appended to preserve existing hosted ABI numbering. + "hosted_env_platform": Host.env_platform!, + "hosted_env_dict": Host.env_dict!, + "hosted_env_set_cwd": Host.env_set_cwd!, + } + targets: { + inputs_dir: "targets/", + x64mac: { inputs: ["libhost.a", app] }, + arm64mac: { inputs: ["libhost.a", app] }, + x64win: { inputs: ["host.lib", "advapi32.lib", "bcrypt.lib", "crypt32.lib", "dbghelp.lib", "iphlpapi.lib", "kernel32.lib", "ncrypt.lib", "ntdll.lib", "ole32.lib", "secur32.lib", "shell32.lib", "user32.lib", "userenv.lib", "ws2_32.lib", 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 Env +import File +import Host +import Http +import IOErr +import InternalSqlite +import Locale +import OsStr +import Path +import Random +import Sleep +import Sqlite +import Stdin +import Stdout import Stderr -import InternalArg +import Tcp +import Tty +import Url +import Utc -main_for_host! : List InternalArg.ArgToAndFromHost => I32 -main_for_host! = |raw_args| - - args = - raw_args - |> List.map(InternalArg.to_os_raw) - |> List.map(Arg.from_os_raw) - - when main!(args) is - Ok({}) -> 0 - Err(Exit(code, msg)) -> - if Str.is_empty(msg) then - code - else - _ = Stderr.line!(msg) - code - - Err(err) -> - err_str = Inspect.to_str(err) - - clean_err_str = - # Inspect adds parentheses around errors, which are unnecessary here. - if Str.starts_with(err_str, "(") and Str.ends_with(err_str, ")") then - err_str - |> Str.replace_first("(", "") - |> Str.replace_last(")", "") - else - err_str - - help_msg = - """ - - Program exited with error: - - ❌ ${clean_err_str} - """ - - _ = Stderr.line!(help_msg) - 1 +main_for_host! : List(OsStr.OsStr) => 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/rust-toolchain.toml b/rust-toolchain.toml index 34d6bc4d..9e701eb6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -5,7 +5,7 @@ # a) Find the latest nightly release that matches RUST_VERSION here: https://github.com/oxalica/rust-overlay/tree/master/manifests/nightly/2024 # b) update `channel = "nightly-OLD_DATE"` below -channel = "1.82.0" # check ^^^ when changing this +channel = "1.83.0" # check ^^^ when changing this components = ["rust-analyzer"] # # channel = "nightly-2024-04-28" # 1.79.0 nightly to be able to use unstable features diff --git a/scripts/build.py b/scripts/build.py new file mode 100755 index 00000000..3e6214f4 --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import os +import platform +import shutil +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +TARGETS = { + "x64mac": "x86_64-apple-darwin", + "arm64mac": "aarch64-apple-darwin", + "x64musl": "x86_64-unknown-linux-musl", + "arm64musl": "aarch64-unknown-linux-musl", +} +ALL_TARGETS = tuple(TARGETS) +ROC_TARGETS = (*ALL_TARGETS, "x64win") +WINDOWS_TARGET = "x86_64-pc-windows-msvc" +WINDOWS_SYSTEM_LIBRARIES = ( + "advapi32.lib", + "bcrypt.lib", + "crypt32.lib", + "dbghelp.lib", + "iphlpapi.lib", + "kernel32.lib", + "ncrypt.lib", + "ntdll.lib", + "ole32.lib", + "secur32.lib", + "shell32.lib", + "user32.lib", + "userenv.lib", + "ws2_32.lib", +) + + +def run(*args: str, env: dict[str, str] | None = None, check: bool = True) -> None: + subprocess.run(args, cwd=ROOT, env=env, check=check) + + +def detect_native_target() -> str: + system = platform.system() + machine = platform.machine().lower() + + if system == "Windows" and machine in {"amd64", "x86_64"}: + return "x64win" + if system == "Darwin": + if machine in {"arm64", "aarch64"}: + return "arm64mac" + if machine in {"x86_64", "amd64"}: + return "x64mac" + if system == "Linux": + if machine in {"aarch64", "arm64"}: + return "arm64musl" + if machine in {"x86_64", "amd64"}: + return "x64musl" + + raise SystemExit(f"Unsupported native platform: {system} {machine}") + + +def musl_build_env(rust_target: str) -> dict[str, str]: + env = os.environ.copy() + zig_targets = { + "x86_64-unknown-linux-musl": "x86_64-linux-musl", + "aarch64-unknown-linux-musl": "aarch64-linux-musl", + } + zig_target = zig_targets.get(rust_target) + if zig_target is None or shutil.which("zig") is None: + return env + + key = rust_target.replace("-", "_") + env["ZIG_CC_TARGET"] = zig_target + env[f"CC_{key}"] = str(ROOT / "ci" / "zig-cc.sh") + env[f"AR_{key}"] = str(ROOT / "ci" / "zig-ar.sh") + env[f"CFLAGS_{key}"] = "-Wno-error" + print(f" (using zig cc for {rust_target})") + return env + + +def install_rust_target(rust_target: str, *, required: bool = False) -> None: + run("rustup", "target", "add", rust_target, check=required) + + +def copy_unix_host(target_name: str, rust_target: str, *, native: bool) -> None: + output_dir = ROOT / "platform" / "targets" / target_name + output_dir.mkdir(parents=True, exist_ok=True) + source = ROOT / "target" / rust_target / "release" / "libhost.a" + + if native and target_name in {"x64mac", "arm64mac"}: + run("cargo", "build", "--locked", "--release", "--lib") + source = ROOT / "target" / "release" / "libhost.a" + else: + run( + "cargo", + "build", + "--locked", + "--release", + "--lib", + "--target", + rust_target, + env=musl_build_env(rust_target), + ) + + destination = output_dir / "libhost.a" + shutil.copy2(source, destination) + print(f" -> {destination.relative_to(ROOT)}") + + +def build_unix_target(target_name: str, *, native: bool = False) -> None: + rust_target = TARGETS[target_name] + qualifier = "native" if native else rust_target + print(f"Building for {target_name} ({qualifier})...") + copy_unix_host(target_name, rust_target, native=native) + + +def find_windows_sdk_lib_dir() -> Path: + program_files = os.environ.get("ProgramFiles(x86)") + if not program_files: + raise SystemExit("ProgramFiles(x86) is not set; cannot locate the Windows SDK") + + sdk_root = Path(program_files) / "Windows Kits" / "10" / "Lib" + if not sdk_root.is_dir(): + raise SystemExit(f"Could not find Windows SDK library directory: {sdk_root}") + candidates = sorted( + ( + directory / "um" / "x64" + for directory in sdk_root.iterdir() + if directory.is_dir() and (directory / "um" / "x64" / "ws2_32.lib").is_file() + ), + reverse=True, + ) + if not candidates: + raise SystemExit(f"Could not find x64 Windows SDK libraries under {sdk_root}") + return candidates[0] + + +def build_windows() -> None: + print(f"Building for x64win ({WINDOWS_TARGET})...") + install_rust_target(WINDOWS_TARGET, required=True) + run( + "cargo", + "build", + "--locked", + "--release", + "--lib", + "--target", + WINDOWS_TARGET, + ) + + output_dir = ROOT / "platform" / "targets" / "x64win" + output_dir.mkdir(parents=True, exist_ok=True) + host_destination = output_dir / "host.lib" + shutil.copy2( + ROOT / "target" / WINDOWS_TARGET / "release" / "host.lib", + host_destination, + ) + print(f" -> {host_destination.relative_to(ROOT)}") + + sdk_lib_dir = find_windows_sdk_lib_dir() + for name in WINDOWS_SYSTEM_LIBRARIES: + source = sdk_lib_dir / name + if not source.is_file(): + raise SystemExit(f"Could not find required Windows SDK library: {source}") + destination = output_dir / name + shutil.copy2(source, destination) + print(f" -> {destination.relative_to(ROOT)}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Build the basic-cli platform host") + parser.add_argument( + "--all", + action="store_true", + help="cross-compile all macOS and Linux targets", + ) + parser.add_argument( + "--target", + choices=ROC_TARGETS, + help="build host inputs for one Roc platform target", + ) + args = parser.parse_args() + + if args.all and args.target: + parser.error("--all and --target are mutually exclusive") + + if args.target: + if args.target == "x64win": + if platform.system() != "Windows": + parser.error("x64win host inputs must be built on Windows") + build_windows() + else: + install_rust_target(TARGETS[args.target], required=True) + build_unix_target(args.target, native=args.target == detect_native_target()) + print("\nBuild complete!") + return + + if args.all: + if platform.system() == "Windows": + parser.error("--all requires a macOS or Linux host") + print("Building for all targets...\n") + for target_name in ALL_TARGETS: + install_rust_target(TARGETS[target_name]) + print() + for target_name in ALL_TARGETS: + build_unix_target(target_name) + print() + print("All targets built successfully!") + return + + target_name = detect_native_target() + print(f"Building for native target: {target_name}\n") + if target_name == "x64win": + build_windows() + else: + if target_name in {"x64musl", "arm64musl"}: + install_rust_target(TARGETS[target_name]) + build_unix_target(target_name, native=True) + print("\nBuild complete!") + + +if __name__ == "__main__": + try: + main() + except subprocess.CalledProcessError as error: + raise SystemExit(error.returncode) from None diff --git a/scripts/bundle.py b/scripts/bundle.py new file mode 100755 index 00000000..34e273f2 --- /dev/null +++ b/scripts/bundle.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import shutil +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PLATFORM_DIR = ROOT / "platform" +LIBRARY_EXTENSIONS = {".a", ".o", ".lib", ".obj"} + + +def relative_platform_path(path: Path) -> str: + return path.relative_to(PLATFORM_DIR).as_posix() + + +def main() -> None: + parser = argparse.ArgumentParser(description="Bundle the basic-cli platform") + parser.add_argument("--output-dir", type=Path, default=ROOT) + args, roc_args = parser.parse_known_args() + + output_dir = args.output_dir + if not output_dir.is_absolute(): + output_dir = ROOT / output_dir + output_dir.mkdir(parents=True, exist_ok=True) + output_dir = output_dir.resolve() + + roc_files = sorted(PLATFORM_DIR.glob("*.roc")) + library_files = sorted( + path + for path in (PLATFORM_DIR / "targets").rglob("*") + if path.is_file() and path.suffix in LIBRARY_EXTENSIONS + ) + bundle_files = [ + *(relative_platform_path(path) for path in roc_files), + *(relative_platform_path(path) for path in library_files), + ] + + print( + f"Bundling {len(roc_files)} .roc files and " + f"{len(library_files)} library files...\n" + ) + print("Files to bundle:") + for path in bundle_files: + print(f" {path}") + print(" THIRD_PARTY_LICENSES.md\n", flush=True) + + license_target = PLATFORM_DIR / "THIRD_PARTY_LICENSES.md" + shutil.copy2(ROOT / "THIRD_PARTY_LICENSES.md", license_target) + try: + subprocess.run( + [ + "roc", + "bundle", + *bundle_files, + "THIRD_PARTY_LICENSES.md", + "--output-dir", + str(output_dir), + *roc_args, + ], + cwd=PLATFORM_DIR, + check=True, + ) + finally: + license_target.unlink(missing_ok=True) + + +if __name__ == "__main__": + try: + main() + except subprocess.CalledProcessError as error: + raise SystemExit(error.returncode) from None diff --git a/scripts/tcp_echo_server.py b/scripts/tcp_echo_server.py new file mode 100755 index 00000000..86b1df17 --- /dev/null +++ b/scripts/tcp_echo_server.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Minimal single-connection TCP echo server used by process tests. + +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 +tests 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) + while True: + conn, _ = server.accept() + with conn: + while True: + data = conn.recv(1024) + if not data: + break + conn.sendall(data) diff --git a/scripts/test.py b/scripts/test.py new file mode 100755 index 00000000..fbcbf6bc --- /dev/null +++ b/scripts/test.py @@ -0,0 +1,639 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import contextlib +import functools +import hashlib +import json +import os +import platform +import re +import select +import shutil +import socket +import subprocess +import sys +import tempfile +import threading +import time +import urllib.request +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Iterator + +from build import ROC_TARGETS, detect_native_target +from update_app_platform_urls import update_apps + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") +if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") + +ROOT = Path(__file__).resolve().parents[1] +SPEC_PATH = ROOT / "scripts" / "test_spec.json" +STAGES = ("fmt", "check", "test", "build", "run") +DEFAULT_ARTIFACT_DIR = ROOT / "dist" / "example-binaries" + + +def declared_targets() -> tuple[str, ...]: + lines = (ROOT / "platform" / "main.roc").read_text(encoding="utf-8").splitlines() + in_targets = False + targets: list[str] = [] + for line in lines: + if not in_targets: + if re.match(r"^\s*targets:\s*\{\s*$", line): + in_targets = True + continue + if re.match(r"^\s*}\s*$", line) and not line.startswith("\t\t"): + break + match = re.match(r"^\s{2,}([A-Za-z0-9_]+):\s*\{\s*inputs:", line.expandtabs(4)) + if match: + targets.append(match.group(1)) + if not targets: + raise SystemExit("platform/main.roc: no platform targets found") + unknown = sorted(set(targets) - set(ROC_TARGETS)) + missing = sorted(set(ROC_TARGETS) - set(targets)) + if unknown or missing: + raise SystemExit(f"Platform/build target mismatch; unknown={unknown}, missing={missing}") + return tuple(targets) + + +def command(*args: str | Path, cwd: Path = ROOT) -> None: + values = [str(arg) for arg in args] + print(f"+ {' '.join(values)}", flush=True) + subprocess.run(values, cwd=cwd, check=True) + + +def roc_extra_args() -> tuple[str, ...]: + args = ("--max-transitive-mb=256",) + return (*args, "--no-cache") if platform.system() == "Windows" else args + + +def load_spec() -> tuple[dict[str, bool], list[dict[str, object]]]: + data = json.loads(SPEC_PATH.read_text(encoding="utf-8")) + defaults = data.get("stages") + apps = data.get("apps") + if not isinstance(defaults, dict) or set(defaults) != set(STAGES): + raise SystemExit(f"{SPEC_PATH}: 'stages' must define {', '.join(STAGES)}") + if not all(isinstance(defaults[name], bool) for name in STAGES): + raise SystemExit(f"{SPEC_PATH}: all stage flags must be booleans") + if not isinstance(apps, list) or not all(isinstance(app, dict) for app in apps): + raise SystemExit(f"{SPEC_PATH}: 'apps' must be a list of objects") + + paths = [app.get("path") for app in apps] + if not all(isinstance(path, str) for path in paths) or len(paths) != len(set(paths)): + raise SystemExit(f"{SPEC_PATH}: every app needs a unique string path") + discovered = { + str(path.relative_to(ROOT).as_posix()) + for directory in (ROOT / "examples",) + for path in directory.glob("*.roc") + } + specified = set(paths) + if discovered != specified: + missing = sorted(discovered - specified) + extra = sorted(specified - discovered) + raise SystemExit(f"Test spec mismatch; missing={missing}, extra={extra}") + for app in apps: + if "run" in app: + raise SystemExit(f"{app['path']}: use the cases array; singular run is not supported") + cases = run_cases(app) + if stage_enabled(defaults, app, "run") and not cases: + raise SystemExit(f"{app['path']}: run is enabled but cases is empty") + return defaults, apps + + +def stage_enabled(defaults: dict[str, bool], app: dict[str, object], stage: str) -> bool: + app_enabled = app.get("enabled", True) + if not isinstance(app_enabled, bool): + raise SystemExit(f"{app['path']}: enabled flag must be a boolean") + enabled = defaults[stage] + overrides = app.get("stages", {}) + if isinstance(overrides, dict) and stage in overrides: + enabled = overrides[stage] + system = platform.system().lower() + platforms = app.get("platforms", {}) + if isinstance(platforms, dict): + platform_overrides = platforms.get(system, {}) + if isinstance(platform_overrides, dict): + platform_enabled = platform_overrides.get("enabled", True) + if not isinstance(platform_enabled, bool): + raise SystemExit(f"{app['path']}: platform enabled flag must be a boolean") + app_enabled = app_enabled and platform_enabled + if stage in platform_overrides: + enabled = platform_overrides[stage] + if not isinstance(enabled, bool): + raise SystemExit(f"{app['path']}: {stage} flag must be a boolean") + return app_enabled and enabled + + +def create_bundle() -> Path: + result = subprocess.run( + [sys.executable, str(ROOT / "scripts" / "bundle.py")], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + ) + print(result.stdout, end="") + matches = re.findall(r"^Created:\s+(.+\.tar\.zst)\s*$", result.stdout, re.MULTILINE) + if not matches: + raise SystemExit("Bundle creation did not report a created archive") + bundle = Path(matches[-1]) + if not bundle.is_absolute(): + bundle = ROOT / bundle + if not bundle.is_file(): + raise SystemExit(f"Bundle creation did not produce an archive: {bundle}") + return bundle.resolve() + + +class BundleServer: + def __init__(self, bundle: Path) -> None: + handler = functools.partial(SimpleHTTPRequestHandler, directory=str(bundle.parent)) + self.server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) + self.url = f"http://127.0.0.1:{self.server.server_port}/{bundle.name}" + + def __enter__(self) -> str: + self.thread.start() + with urllib.request.urlopen( + urllib.request.Request(self.url, method="HEAD"), timeout=5 + ): + pass + return self.url + + def __exit__(self, *_: object) -> None: + self.server.shutdown() + self.server.server_close() + self.thread.join() + + +def expand(value: str, source: Path) -> str: + return value.format(root=ROOT, source=source, source_dir=source.parent) + + +def wait_for_port(port: int, process: subprocess.Popen[bytes], timeout: float = 5) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if process.poll() is not None: + raise SystemExit(f"Helper server on port {port} exited early") + with socket.socket() as sock: + sock.settimeout(0.1) + if sock.connect_ex(("127.0.0.1", port)) == 0: + return + time.sleep(0.05) + raise SystemExit(f"Helper server did not listen on port {port}") + + +@contextlib.contextmanager +def helper_server(name: str | None) -> Iterator[None]: + if name is None: + yield + return + if name == "http": + suffix = ".exe" if platform.system() == "Windows" else "" + args = [str(ROOT / "ci" / "rust_http_server" / "target" / "release" / f"rust_http_server{suffix}")] + port = 9000 + elif name == "tcp": + args = [sys.executable, str(ROOT / "scripts" / "tcp_echo_server.py")] + port = 8085 + else: + raise SystemExit(f"Unknown helper server: {name}") + process = subprocess.Popen(args, cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + try: + wait_for_port(port, process) + yield + finally: + process.terminate() + try: + process.wait(timeout=3) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + + +def pipe_process( + args: list[str], cwd: Path, env: dict[str, str] | dict[bytes, bytes], + stdin: bytes, timeout: float, +) -> tuple[int, str, str]: + result = subprocess.run( + args, cwd=cwd, env=env, input=stdin, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, timeout=timeout, + ) + return ( + result.returncode, + result.stdout.decode("utf-8", errors="replace"), + result.stderr.decode("utf-8", errors="replace"), + ) + + +def pty_process( + args: list[str], cwd: Path, env: dict[str, str], stdin: bytes, timeout: float, + input_delay: float, +) -> tuple[int, str]: + if os.name != "posix": + raise SystemExit("PTY execution is only available on POSIX hosts") + import pty + + master, slave = pty.openpty() + process = subprocess.Popen(args, cwd=cwd, env=env, stdin=slave, stdout=slave, stderr=slave) + os.close(slave) + output = bytearray() + deadline = time.monotonic() + timeout + try: + time.sleep(0.25) + for byte in stdin: + os.write(master, bytes([byte])) + time.sleep(input_delay) + while process.poll() is None: + if time.monotonic() >= deadline: + process.kill() + raise SystemExit(f"Timed out after {timeout}s: {' '.join(args)}") + readable, _, _ = select.select([master], [], [], 0.1) + if readable: + try: + chunk = os.read(master, 65536) + if not chunk: + break + output.extend(chunk) + except OSError: + break + while True: + readable, _, _ = select.select([master], [], [], 0) + if not readable: + break + try: + chunk = os.read(master, 65536) + if not chunk: + break + output.extend(chunk) + except OSError: + break + return process.wait(), output.decode("utf-8", errors="replace") + finally: + os.close(master) + if process.poll() is None: + process.kill() + process.wait() + + +def make_environment(run_spec: dict[str, object], source: Path) -> dict[str, str] | dict[bytes, bytes]: + env = os.environ.copy() + for name in run_spec.get("unset_env", []): + env.pop(str(name), None) + values = run_spec.get("env", {}) + if not isinstance(values, dict): + raise SystemExit(f"{source}: run.env must be an object") + for name, value in values.items(): + env[str(name)] = expand(str(value), source) + non_utf8 = run_spec.get("non_utf8_env", {}) + if non_utf8 and os.name == "posix": + byte_env = {name.encode(): value.encode() for name, value in env.items()} + for name, value in non_utf8.items(): + byte_env[str(name).encode()] = bytes.fromhex(str(value)) + return byte_env + return env + + +def verify_text(source: Path, case_name: str, stream: str, output: str, + contains: object, regexes: object) -> None: + normalized = output.replace("\r\n", "\n").replace("\r", "\n") + if "[ROC CRASHED]" in normalized: + raise SystemExit(f"{source}: runtime crash\n{normalized}") + if not isinstance(contains, list) or not isinstance(regexes, list): + raise SystemExit(f"{source} [{case_name}]: {stream} assertions must be arrays") + for expected in contains: + if str(expected) not in normalized: + raise SystemExit( + f"{source} [{case_name}]: missing {stream} output {expected!r}" + f"\n--- {stream} ---\n{normalized}" + ) + for pattern in regexes: + if re.search(str(pattern), normalized, re.MULTILINE) is None: + raise SystemExit( + f"{source} [{case_name}]: {stream} did not match {pattern!r}" + f"\n--- {stream} ---\n{normalized}" + ) + + +def verify_output(source: Path, case_name: str, stdout: str, stderr: str, + run_spec: dict[str, object], *, pty: bool) -> None: + combined = stdout if pty else stdout + stderr + verify_text(source, case_name, "combined output", combined, + run_spec.get("contains", []), run_spec.get("regex", [])) + if pty and any(name in run_spec for name in ( + "stdout_contains", "stdout_regex", "stderr_contains", "stderr_regex" + )): + raise SystemExit(f"{source} [{case_name}]: PTY cases cannot assert separate streams") + if not pty: + verify_text(source, case_name, "stdout", stdout, + run_spec.get("stdout_contains", []), run_spec.get("stdout_regex", [])) + verify_text(source, case_name, "stderr", stderr, + run_spec.get("stderr_contains", []), run_spec.get("stderr_regex", [])) + + +def run_cases(app: dict[str, object]) -> list[dict[str, object]]: + cases = app.get("cases", []) + if not isinstance(cases, list) or not all(isinstance(case, dict) for case in cases): + raise SystemExit(f"{app['path']}: cases must be a list of objects") + names = [case.get("name") for case in cases] + if not all(isinstance(name, str) and name for name in names) or len(names) != len(set(names)): + raise SystemExit(f"{app['path']}: every run case needs a unique non-empty name") + return cases + + +def case_enabled(case: dict[str, object]) -> bool: + enabled = case.get("enabled", True) + if not isinstance(enabled, bool): + raise SystemExit(f"Run case {case.get('name')}: enabled must be a boolean") + platforms = case.get("platforms", {}) + if isinstance(platforms, dict): + override = platforms.get(platform.system().lower(), {}) + if isinstance(override, dict): + platform_enabled = override.get("enabled", True) + if not isinstance(platform_enabled, bool): + raise SystemExit(f"Run case {case.get('name')}: platform enabled must be boolean") + enabled = enabled and platform_enabled + return enabled + + +def run_binary(app: dict[str, object], binary: Path, run_spec: dict[str, object]) -> None: + source = ROOT / str(app["path"]) + case_name = str(run_spec["name"]) + print(f"\n--- {app['path']} [{case_name}] ---") + args = [str(binary), *(expand(str(value), source) for value in run_spec.get("args", []))] + temporary_cwd = tempfile.TemporaryDirectory(prefix="basic-cli-case-") if run_spec.get("temp_cwd") else None + cwd = Path(temporary_cwd.name) if temporary_cwd else Path(expand(str(run_spec.get("cwd", "{root}")), source)) + if "stdin_hex" in run_spec: + stdin = bytes.fromhex(str(run_spec["stdin_hex"])) + else: + stdin = str(run_spec.get("stdin", "")).encode() + timeout = float(run_spec.get("timeout", 7)) + fixtures = run_spec.get("fixtures", []) + fixture_targets: list[Path] = [] + for fixture in fixtures: + target = ROOT / str(fixture["target"]) + shutil.copy2(ROOT / str(fixture["source"]), target) + fixture_targets.append(target) + try: + with helper_server(run_spec.get("helper")): + env = make_environment(run_spec, source) + if run_spec.get("pty"): + if not isinstance(env, dict) or any(isinstance(key, bytes) for key in env): + raise SystemExit(f"{source}: PTY tests require a text environment") + exit_code, stdout = pty_process( + args, cwd, env, stdin, timeout, + float(run_spec.get("input_delay", 0.08)), + ) + stderr = "" + else: + exit_code, stdout, stderr = pipe_process(args, cwd, env, stdin, timeout) + if stdout: + print(stdout, end="" if stdout.endswith("\n") else "\n") + if stderr: + print(stderr, end="" if stderr.endswith("\n") else "\n", file=sys.stderr) + expected_exit = int(run_spec.get("exit_code", 0)) + if exit_code != expected_exit: + raise SystemExit(f"{source} [{case_name}]: exited with {exit_code}, expected {expected_exit}") + verify_output(source, case_name, stdout, stderr, run_spec, pty=bool(run_spec.get("pty"))) + finally: + for target in fixture_targets: + target.unlink(missing_ok=True) + if temporary_cwd is not None: + temporary_cwd.cleanup() + + +def run_stage( + stage: str, defaults: dict[str, bool], apps: list[dict[str, object]], + binaries: dict[str, Path], build_dir: Path, target: str, +) -> None: + print(f"\n=== {stage.upper()} ===") + for app in apps: + path = str(app["path"]) + source = ROOT / path + if not stage_enabled(defaults, app, stage): + reason = f" ({app['reason']})" if app.get("reason") else "" + print(f"SKIP {stage}: {path}{reason}") + continue + if stage == "fmt": + command("roc", "fmt", "--check", source) + elif stage == "check": + command("roc", "check", source, *roc_extra_args()) + elif stage == "test": + command("roc", "test", source, *roc_extra_args()) + elif stage == "build": + suffix = ".exe" if target == "x64win" else "" + binary = build_dir / target / f"{source.stem}{suffix}" + binary.parent.mkdir(parents=True, exist_ok=True) + command( + "roc", "build", source, f"--target={target}", + f"--output={binary}", *roc_extra_args(), + ) + binaries[path] = binary + elif stage == "run": + binary = binaries.get(path) + if binary is None: + raise SystemExit(f"{path}: run is enabled but build is disabled") + for run_spec in run_cases(app): + if case_enabled(run_spec): + run_binary(app, binary, run_spec) + else: + print(f"SKIP run case: {path} [{run_spec['name']}]") + + +def spec_hash() -> str: + return hashlib.sha256(SPEC_PATH.read_bytes()).hexdigest() + + +def sources_hash(paths: object, contents: dict[Path, bytes] | None = None) -> str: + if not isinstance(paths, (set, list, tuple, dict)): + raise SystemExit("Cannot hash artifact sources") + names = paths.keys() if isinstance(paths, dict) else paths + digest = hashlib.sha256() + for name in sorted(str(value) for value in names): + digest.update(name.encode("utf-8")) + digest.update(b"\0") + path = ROOT / name + digest.update(contents[path] if contents is not None else path.read_bytes()) + digest.update(b"\0") + return digest.hexdigest() + + +def write_manifest(target: str, binaries: dict[str, Path], artifact_dir: Path, + source_sha256: str) -> None: + manifest = { + "target": target, + "spec_sha256": spec_hash(), + "sources_sha256": source_sha256, + "binaries": { + source: str(binary.relative_to(artifact_dir).as_posix()) + for source, binary in sorted(binaries.items()) + }, + } + path = artifact_dir / target / "manifest.json" + path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + + +def load_artifact_binaries(target: str, artifact_dir: Path, + defaults: dict[str, bool], apps: list[dict[str, object]]) -> dict[str, Path]: + manifest_path = artifact_dir / target / "manifest.json" + if not manifest_path.is_file(): + raise SystemExit(f"Missing artifact manifest: {manifest_path}") + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + if manifest.get("target") != target: + raise SystemExit(f"{manifest_path}: target does not match {target}") + if manifest.get("spec_sha256") != spec_hash(): + raise SystemExit(f"{manifest_path}: artifacts were built from a different test spec") + entries = manifest.get("binaries") + if not isinstance(entries, dict): + raise SystemExit(f"{manifest_path}: binaries must be an object") + expected = { + str(app["path"]) for app in apps if stage_enabled(defaults, app, "build") + } + if manifest.get("sources_sha256") != sources_hash(expected): + raise SystemExit(f"{manifest_path}: artifacts were built from different example sources") + if set(entries) != expected: + raise SystemExit( + f"{manifest_path}: binary mismatch; missing={sorted(expected - set(entries))}, " + f"extra={sorted(set(entries) - expected)}" + ) + binaries = {source: artifact_dir / str(relative) for source, relative in entries.items()} + missing = sorted(source for source, binary in binaries.items() if not binary.is_file()) + if missing: + raise SystemExit(f"{manifest_path}: missing binary files for {missing}") + if os.name == "posix": + for binary in binaries.values(): + binary.chmod(binary.stat().st_mode | 0o111) + return binaries + + +def run_artifact_cases(target: str, artifact_dir: Path) -> None: + defaults, apps = load_spec() + binaries = load_artifact_binaries(target, artifact_dir, defaults, apps) + if any( + stage_enabled(defaults, app, "run") + and any(case_enabled(case) and case.get("helper") == "http" for case in run_cases(app)) + for app in apps + ): + command("cargo", "build", "--locked", "--release", cwd=ROOT / "ci" / "rust_http_server") + run_stage("run", defaults, apps, binaries, artifact_dir, target) + + +def run_suite(bundle_url: str, operations: set[str], target: str, artifact_dir: Path) -> None: + defaults, apps = load_spec() + sources = [ROOT / str(app["path"]) for app in apps] + backups = {path: path.read_bytes() for path in sources} + original_sources_sha256 = sources_hash( + [str(path.relative_to(ROOT).as_posix()) for path in sources], backups + ) + try: + update_apps([ROOT / "examples"], bundle_url) + if "run" in operations and any( + stage_enabled(defaults, app, "run") + and any(case_enabled(case) and case.get("helper") == "http" for case in run_cases(app)) + for app in apps + ): + command("cargo", "build", "--locked", "--release", cwd=ROOT / "ci" / "rust_http_server") + binaries: dict[str, Path] = {} + if "validate" in operations: + print("\n=== PLATFORM TEST ===") + command("roc", "test", ROOT / "platform" / "main.roc", *roc_extra_args()) + for stage in ("fmt", "check", "test"): + if "validate" in operations: + run_stage(stage, defaults, apps, binaries, artifact_dir, target) + if "build" in operations: + run_stage("build", defaults, apps, binaries, artifact_dir, target) + write_manifest(target, binaries, artifact_dir, original_sources_sha256) + if "run" in operations: + if not binaries: + binaries = load_artifact_binaries(target, artifact_dir, defaults, apps) + run_stage("run", defaults, apps, binaries, artifact_dir, target) + finally: + for path, contents in backups.items(): + path.write_bytes(contents) + cleanup_test_files() + + +def cleanup_test_files() -> None: + for directory in (ROOT / "examples",): + for pattern in ("*.e2e.db", "*.bak"): + for path in directory.glob(pattern): + path.unlink(missing_ok=True) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Build and test basic-cli against a bundle") + parser.add_argument("--bundle-path", type=Path) + parser.add_argument("--bundle-url", default=os.environ.get("BUNDLE_URL")) + parser.add_argument("--no-build", action="store_true", default=os.environ.get("NO_BUILD") == "1") + parser.add_argument( + "--operation", choices=("all", "validate", "build", "run"), default="all", + help="run the full native workflow or one reusable CI operation", + ) + parser.add_argument("--target", choices=declared_targets()) + parser.add_argument("--artifact-dir", type=Path, default=DEFAULT_ARTIFACT_DIR) + args = parser.parse_args() + if args.bundle_path and args.bundle_url: + parser.error("--bundle-path and --bundle-url are mutually exclusive") + target = args.target or detect_native_target() + artifact_dir = args.artifact_dir.resolve() + if args.operation == "run": + if target != detect_native_target(): + raise SystemExit(f"Cannot run {target} artifacts on native target {detect_native_target()}") + run_artifact_cases(target, artifact_dir) + print("\n=== All native run cases passed! ===") + return + + if shutil.which("roc") is None: + raise SystemExit("'roc' was not found on PATH") + print(f"Using roc version: {subprocess.check_output(['roc', 'version'], text=True).strip()}") + if not args.no_build and not args.bundle_path: + print("\n=== Building platform ===") + command(sys.executable, ROOT / "scripts" / "build.py", "--target", target) + + operations = { + "all": {"validate", "build", "run"}, + "validate": {"validate"}, + "build": {"build"}, + }[args.operation] + if "run" in operations and target != detect_native_target(): + raise SystemExit(f"Cannot run {target} artifacts on native target {detect_native_target()}") + + generated_bundle: Path | None = None + try: + if args.bundle_url: + print(f"\n=== Using provided bundle ===\nBundle: {args.bundle_url}") + run_suite(args.bundle_url, operations, target, artifact_dir) + else: + bundle = args.bundle_path + if bundle is None: + print("\n=== Bundling platform ===") + bundle = generated_bundle = create_bundle() + elif not bundle.is_absolute(): + bundle = ROOT / bundle + bundle = bundle.resolve() + if not bundle.is_file(): + raise SystemExit(f"Bundle does not exist: {bundle}") + with BundleServer(bundle) as bundle_url: + print(f"Bundle: {bundle_url}") + run_suite(bundle_url, operations, target, artifact_dir) + finally: + if generated_bundle is not None: + generated_bundle.unlink(missing_ok=True) + cleanup_test_files() + messages = { + "all": "All native checks and run cases passed", + "validate": "All source validation stages passed", + "build": f"All examples built for {target}", + } + print(f"\n=== {messages[args.operation]}! ===") + + +if __name__ == "__main__": + try: + main() + except subprocess.CalledProcessError as error: + raise SystemExit(error.returncode) from None + except subprocess.TimeoutExpired as error: + raise SystemExit(f"Timed out after {error.timeout}s: {' '.join(error.cmd)}") from None diff --git a/scripts/test_spec.json b/scripts/test_spec.json new file mode 100644 index 00000000..2bcac8e5 --- /dev/null +++ b/scripts/test_spec.json @@ -0,0 +1,418 @@ +{ + "stages": { + "fmt": true, + "check": true, + "test": true, + "build": true, + "run": true + }, + "apps": [ + { + "path": "examples/bytes-stdin-stdout.roc", + "cases": [ + { + "name": "happy", + "stdin": "someinput\n", + "stdout_contains": ["someinput\n"], + "stderr_contains": ["Copied 10 bytes from stdin to stdout."] + } + ] + }, + { + "path": "examples/command-line-args.roc", + "cases": [ + { + "name": "one-argument", + "args": ["foo"], + "contains": ["received argument: foo", "back to OsStr: OsStr."], + "regex": [ + "(?:Unix argument, bytes|Windows argument, UTF-16 code units): \\[102, 111, 111\\]" + ] + }, + { + "name": "missing-argument", + "exit_code": 1, + "contains": ["Program exited with error: MissingArgument"] + } + ] + }, + { + "path": "examples/command.roc", + "platforms": { "windows": { "run": false } }, + "reason": "Runtime scenario invokes Unix commands", + "cases": [ + { + "name": "happy", + "contains": ["Hello", "Command config: Cmd({", "Exit code: 1", "stdout_bytes: [72, 105, 10]"] + } + ] + }, + { + "path": "examples/dir.roc", + "cases": [ + { + "name": "workspace-lifecycle", + "temp_cwd": true, + "contains": ["Workspace entries:", "Workspace cleaned up."] + } + ] + }, + { + "path": "examples/env-var.roc", + "cases": [ + { + "name": "both-variables", + "env": { "EDITOR": "nano", "LETTERS": "a,c,e,j" }, + "contains": [ + "Your favorite editor is nano!", + "Your favorite letters are: a c e j" + ] + }, + { + "name": "missing-editor", + "unset_env": ["EDITOR", "LETTERS"], + "exit_code": 1, + "contains": ["VarNotFound(OsStr."] + }, + { + "name": "missing-letters", + "unset_env": ["LETTERS"], + "env": { "EDITOR": "nano" }, + "exit_code": 1, + "contains": ["VarNotFound(OsStr."] + } + ] + }, + { + "path": "examples/error-handling.roc", + "cases": [ + { + "name": "handled-not-found", + "contains": [ + "Expected error: Path not found (NotFound)", + "Expected error: Path is a directory (IsADirectory)", + "Expected error: Path is not a directory (NotADirectory)", + "test-file.txt contains: Hello from error-handling example!" + ] + } + ] + }, + { + "path": "examples/file-accessed-modified-created-time.roc", + "cases": [ + { + "name": "happy", + "args": ["{root}/LICENSE"], + "regex": [ + "LICENSE file time metadata:\\n Modified: [0-9]+ ms since epoch\\n Accessed: [0-9]+ ms since epoch\\n Created: ([0-9]+ ms since epoch|unsupported)" + ] + } + ] + }, + { + "path": "examples/file-permissions.roc", + "cases": [ + { + "name": "happy", + "args": ["{root}/LICENSE"], + "contains": [ + "LICENSE file permissions:", + "Executable: False", + "Readable: True", + "Writable: True" + ] + } + ] + }, + { + "path": "examples/file-read-buffered.roc", + "cases": [ + { + "name": "happy", + "regex": [ + "Done reading file: \\{ bytes_read: [0-9]+, lines_read: [0-9]+ \\}" + ] + } + ] + }, + { + "path": "examples/file-read-write.roc", + "cases": [ + { + "name": "happy", + "temp_cwd": true, + "contains": [ + "Writing a string to out.txt", + "I read the file back. Its contents are: \"a string!\"" + ] + } + ] + }, + { + "path": "examples/file-size.roc", + "cases": [ + { + "name": "existing-file", + "args": ["{root}/LICENSE"], + "regex": ["LICENSE is [0-9]+ bytes"] + }, + { + "name": "missing-file", + "args": ["missing.txt"], + "temp_cwd": true, + "exit_code": 1, + "contains": ["PathErr(NotFound)"] + } + ] + }, + { + "path": "examples/hello-world.roc", + "cases": [{ "name": "happy", "contains": ["Hello, World!"] }] + }, + { + "path": "examples/hello.roc", + "cases": [ + { "name": "default-name", "contains": ["Hello, friend, from basic-cli!"] }, + { "name": "given-name", "args": ["Roc"], "contains": ["Hello, Roc, from basic-cli!"] } + ] + }, + { + "path": "examples/http-client.roc", + "cases": [ + { + "name": "server-available", + "helper": "http", + "timeout": 15, + "contains": [ + "I received 'Hello utf8' from the server.", + "send_json! echoed: { foo: \"Hello Json!\" }.", + "invalid UTF-8 was rejected.", + "invalid request URL was rejected." + ] + }, + { + "name": "connection-refused", + "exit_code": 1, + "contains": ["GetUtf8Failed"] + } + ] + }, + { + "path": "examples/http.roc", + "cases": [ + { + "name": "server-available", + "helper": "http", + "timeout": 15, + "contains": [ + "I received 'Hello utf8' from the server.", + "Basic CLI HTTP test" + ] + }, + { + "name": "connection-refused", + "exit_code": 1, + "contains": ["GetUtf8Failed"] + } + ] + }, + { + "path": "examples/locale.roc", + "cases": [ + { + "name": "happy", + "regex": [ + "The most preferred locale for this system or application: .+", + "All available locales for this system or application: \\[.*\\]" + ], + "contains": ["Locale JSON example: \"en-US\""] + } + ] + }, + { + "path": "examples/path.roc", + "cases": [ + { + "name": "existing-source", + "args": ["{source}"], + "contains": ["Debug: Path.", "Filename: path.roc", "Extension: roc", "Type: IsFile"] + } + ] + }, + { + "path": "examples/print.roc", + "cases": [ + { + "name": "happy", + "stdout_contains": [ + "Hello, world!", + "No newline after me.Foo", + "Baz" + ], + "stderr_contains": ["Hello, error!", "Err with no newline after."] + } + ] + }, + { + "path": "examples/random.roc", + "cases": [ + { + "name": "happy", + "regex": ["Random U64 seed is: [0-9]+", "Random U32 seed is: [0-9]+"] + } + ] + }, + { + "path": "examples/sqlite-basic.roc", + "cases": [ + { + "name": "existing-database", + "env": { "DB_PATH": "{root}/examples/todos.db" }, + "contains": ["All Todos:", "Share my ❤️ for Roc", "Completed Todos:"] + }, + { + "name": "cannot-open-database", + "env": { "DB_PATH": "{root}/missing/sqlite.db" }, + "exit_code": 1, + "contains": ["QueryTodosFailed(SqliteErr(CanNotOpen"] + } + ] + }, + { + "path": "examples/sqlite-everything.roc", + "cases": [ + { + "name": "existing-database", + "fixtures": [ + { + "source": "examples/todos2.db", + "target": "examples/todos2.e2e.db" + } + ], + "env": { "DB_PATH": "{root}/examples/todos2.e2e.db" }, + "contains": [ + "Row count: 3", + "Todos sorted by length of task description:" + ] + }, + { + "name": "cannot-open-database", + "env": { "DB_PATH": "{root}/missing/sqlite.db" }, + "exit_code": 1, + "contains": ["QueryAllTodosFailed(SqliteErr(CanNotOpen"] + } + ] + }, + { + "path": "examples/stdin-basic.roc", + "cases": [ + { + "name": "two-lines", + "stdin": "John\nDoe\n", + "contains": [ + "What's your first name?", + "What's your last name?", + "Hi, John Doe! 👋" + ] + }, + { + "name": "end-of-input", + "exit_code": 1, + "stderr_contains": ["MissingFirstName"] + } + ] + }, + { + "path": "examples/stdin-pipe.roc", + "cases": [ + { + "name": "valid-utf8", + "stdin": "hey", + "contains": ["This is what you piped in: \"hey\""] + }, + { + "name": "invalid-utf8", + "stdin_hex": "fffe", + "exit_code": 1, + "contains": ["BadUtf8({ index: 0, problem: InvalidStartByte })"] + } + ] + }, + { + "path": "examples/tcp-client.roc", + "cases": [ + { + "name": "server-available", + "helper": "tcp", + "stdin": "Hi\n", + "contains": ["Connected!", "< Hi"] + }, + { + "name": "connection-refused", + "exit_code": 1, + "contains": ["ConnectFailed"] + } + ] + }, + { + "path": "examples/temp-dir.roc", + "cases": [{ "name": "happy", "regex": ["The temp dir path is .+"] }] + }, + { + "path": "examples/terminal-app-snake.roc", + "platforms": { "windows": { "run": false } }, + "reason": "Python standard library has no Windows ConPTY support", + "cases": [ + { + "name": "quit-command", + "pty": true, + "stdin": "q", + "contains": ["Game Over"] + } + ] + }, + { + "path": "examples/time.roc", + "cases": [ + { + "name": "one-second-sleep", + "timeout": 10, + "regex": [ + "Started at [0-9]{4}-[0-9]{2}-[0-9]{2}T", + "Completed in [0-9]+ ms \\([0-9]+ ns\\)" + ] + } + ] + }, + { + "path": "examples/tty.roc", + "platforms": { "windows": { "run": false } }, + "reason": "Python standard library has no Windows ConPTY support", + "cases": [ + { + "name": "raw-mode", + "pty": true, + "stdin": "q", + "contains": [ + "Tty: enabling raw mode", + "Tty: disabling raw mode", + "Read 1 byte(s)." + ] + } + ] + }, + { + "path": "examples/url.roc", + "cases": [ + { + "name": "search-request", + "contains": [ + "Request URL: https://api.example.com/v1/search?q=roc+lang&page=1#results", + "Debug URL: Url(\"https://api.example.com/v1/search?q=roc+lang&page=1#results\")", + "JSON URL: \"https://api.example.com/v1/search?q=roc+lang&page=1#results\"" + ] + } + ] + } + ] +} diff --git a/scripts/update_app_platform_urls.py b/scripts/update_app_platform_urls.py new file mode 100755 index 00000000..dc8d7a29 --- /dev/null +++ b/scripts/update_app_platform_urls.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PLATFORM_RE = re.compile(r'(?m)(\bplatform\s+)"[^"]+"') + + +def update_apps(paths: list[Path], platform_url: str) -> list[Path]: + roc_files: list[Path] = [] + for path in paths: + if path.is_dir(): + roc_files.extend(sorted(path.glob("*.roc"))) + elif path.suffix == ".roc": + roc_files.append(path) + else: + raise SystemExit(f"Expected a Roc app or directory: {path}") + + if not roc_files: + raise SystemExit("No Roc apps found") + + updated: list[Path] = [] + for roc_file in roc_files: + source = roc_file.read_text(encoding="utf-8") + rewritten, count = PLATFORM_RE.subn( + lambda match: f'{match.group(1)}"{platform_url}"', + source, + count=1, + ) + if count != 1: + raise SystemExit( + f"Expected exactly one platform URL in {roc_file}, found {count}" + ) + if rewritten != source: + roc_file.write_text(rewritten, encoding="utf-8", newline="\n") + updated.append(roc_file) + + return updated + + +def display_path(path: Path) -> str: + try: + return str(path.relative_to(ROOT)) + except ValueError: + return str(path) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--platform-url", required=True) + parser.add_argument("paths", nargs="+", type=Path) + args = parser.parse_args() + + updated = update_apps(args.paths, args.platform_url) + if updated: + print("Updated app platform URLs:") + for path in updated: + print(f"- {display_path(path)}") + else: + print("App platform URLs are already up to date.") + + +if __name__ == "__main__": + main() diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 00000000..6e1c6f16 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,291 @@ +use core::mem::ManuallyDrop; +use std::io; + +use crate::roc_platform_abi::*; +use crate::{os_string_from_native, roc_host, roc_u8_list_from_slice, NativeOsStr}; + +type CmdExitResult = HostCmdExecExitCodeResult; +type CmdExitResultPayload = HostCmdExecExitCodeResultPayload; +type CmdExitResultTag = HostCmdExecExitCodeResultTag; +type CmdOutputResult = HostCmdExecOutputResult; +type CmdOutputResultPayload = HostCmdExecOutputResultPayload; +type CmdOutputResultTag = HostCmdExecOutputResultTag; +type CmdOutputError = FailedToGetExitCodeOrNonZeroExitCode; +type CmdOutputErrorPayload = FailedToGetExitCodeOrNonZeroExitCodePayload; +type CmdOutputErrorTag = FailedToGetExitCodeOrNonZeroExitCodeTag; +type CmdOutputFailure = HostCmdExecOutputErrNonZeroExitCode; +type CmdOutputSuccess = HostCmdExecOutputOk; +type Cmd = HostCmdExecExitCodeArgs; + +fn cmd_io_err_other(message: &str, roc_host: &RocHost) -> HostIOErr { + HostIOErr { + payload: HostIOErrPayload { + other: ManuallyDrop::new(RocStr::from_str(message, roc_host)), + }, + tag: HostIOErrTag::Other, + } +} + +fn cmd_io_err_from_io(error: &io::Error, roc_host: &RocHost) -> HostIOErr { + match error.kind() { + io::ErrorKind::AlreadyExists => HostIOErr { + payload: HostIOErrPayload { already_exists: [] }, + tag: HostIOErrTag::AlreadyExists, + }, + io::ErrorKind::BrokenPipe => HostIOErr { + payload: HostIOErrPayload { broken_pipe: [] }, + tag: HostIOErrTag::BrokenPipe, + }, + io::ErrorKind::Interrupted => HostIOErr { + payload: HostIOErrPayload { interrupted: [] }, + tag: HostIOErrTag::Interrupted, + }, + io::ErrorKind::IsADirectory => HostIOErr { + payload: HostIOErrPayload { is_adirectory: [] }, + tag: HostIOErrTag::IsADirectory, + }, + io::ErrorKind::NotFound => HostIOErr { + payload: HostIOErrPayload { not_found: [] }, + tag: HostIOErrTag::NotFound, + }, + io::ErrorKind::NotADirectory => HostIOErr { + payload: HostIOErrPayload { not_adirectory: [] }, + tag: HostIOErrTag::NotADirectory, + }, + io::ErrorKind::OutOfMemory => HostIOErr { + payload: HostIOErrPayload { out_of_memory: [] }, + tag: HostIOErrTag::OutOfMemory, + }, + io::ErrorKind::PermissionDenied => HostIOErr { + payload: HostIOErrPayload { + permission_denied: [], + }, + tag: HostIOErrTag::PermissionDenied, + }, + io::ErrorKind::Unsupported => HostIOErr { + payload: HostIOErrPayload { unsupported: [] }, + tag: HostIOErrTag::Unsupported, + }, + _ => cmd_io_err_other(&error.to_string(), roc_host), + } +} + +fn cmd_output_io_err_other(message: &str, roc_host: &RocHost) -> IOErr { + IOErr { + payload: IOErrPayload { + other: ManuallyDrop::new(RocStr::from_str(message, roc_host)), + }, + tag: IOErrTag::Other, + } +} + +fn cmd_output_io_err_from_io(error: &io::Error, roc_host: &RocHost) -> IOErr { + match error.kind() { + io::ErrorKind::AlreadyExists => IOErr { + payload: IOErrPayload { already_exists: [] }, + tag: IOErrTag::AlreadyExists, + }, + io::ErrorKind::BrokenPipe => IOErr { + payload: IOErrPayload { broken_pipe: [] }, + tag: IOErrTag::BrokenPipe, + }, + io::ErrorKind::Interrupted => IOErr { + payload: IOErrPayload { interrupted: [] }, + tag: IOErrTag::Interrupted, + }, + io::ErrorKind::IsADirectory => IOErr { + payload: IOErrPayload { is_adirectory: [] }, + tag: IOErrTag::IsADirectory, + }, + io::ErrorKind::NotFound => IOErr { + payload: IOErrPayload { not_found: [] }, + tag: IOErrTag::NotFound, + }, + io::ErrorKind::NotADirectory => IOErr { + payload: IOErrPayload { not_adirectory: [] }, + tag: IOErrTag::NotADirectory, + }, + io::ErrorKind::OutOfMemory => IOErr { + payload: IOErrPayload { out_of_memory: [] }, + tag: IOErrTag::OutOfMemory, + }, + io::ErrorKind::PermissionDenied => IOErr { + payload: IOErrPayload { + permission_denied: [], + }, + tag: IOErrTag::PermissionDenied, + }, + io::ErrorKind::Unsupported => IOErr { + payload: IOErrPayload { unsupported: [] }, + tag: IOErrTag::Unsupported, + }, + _ => cmd_output_io_err_other(&error.to_string(), roc_host), + } +} + +fn take_arg_list( + list: &RocList, + roc_host: &RocHost, +) -> io::Result> { + let mut values = Vec::with_capacity(list.len()); + let mut first_error = None; + + for item in list.as_slice() { + match os_string_from_native(*item, roc_host) { + Ok(value) => values.push(value), + Err(error) => { + if first_error.is_none() { + first_error = Some(error); + } + } + } + } + + unsafe { list.decref(roc_host) }; + + match first_error { + Some(error) => Err(error), + None => Ok(values), + } +} + +fn cmd_to_std(cmd: &Cmd, roc_host: &RocHost) -> io::Result { + let program = os_string_from_native(cmd.program, roc_host); + let args = take_arg_list(&cmd.args, roc_host); + let envs = take_arg_list(&cmd.envs, roc_host); + + let mut std_cmd = std::process::Command::new(program?); + + for arg in args? { + std_cmd.arg(arg); + } + + if cmd.clear_envs { + std_cmd.env_clear(); + } + + let envs = envs?; + for chunk in envs.chunks(2) { + if let [key, value] = chunk { + std_cmd.env(key, value); + } + } + + Ok(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: CmdOutputError) -> CmdOutputResult { + CmdOutputResult { + payload: CmdOutputResultPayload { + err: ManuallyDrop::new(error), + }, + tag: CmdOutputResultTag::Err, + } +} + +fn cmd_output_nonzero_error(value: CmdOutputFailure) -> CmdOutputError { + CmdOutputError { + payload: CmdOutputErrorPayload { + non_zero_exit_code: ManuallyDrop::new(value), + }, + tag: CmdOutputErrorTag::NonZeroExitCode, + } +} + +fn cmd_output_failed_to_get_exit_code(error: IOErr) -> CmdOutputError { + CmdOutputError { + payload: CmdOutputErrorPayload { + failed_to_get_exit_code: ManuallyDrop::new(error), + }, + tag: CmdOutputErrorTag::FailedToGetExitCode, + } +} + +#[no_mangle] +pub extern "C" fn hosted_cmd_host_exec_exit_code(cmd: Cmd) -> CmdExitResult { + let roc_host = roc_host(); + let mut std_cmd = match cmd_to_std(&cmd, roc_host) { + Ok(cmd) => cmd, + Err(error) => return try_cmd_exit_err(cmd_io_err_from_io(&error, 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 = match cmd_to_std(&cmd, roc_host) { + Ok(cmd) => cmd, + Err(error) => { + return try_cmd_output_err(cmd_output_failed_to_get_exit_code( + cmd_output_io_err_from_io(&error, 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(cmd_output_nonzero_error(CmdOutputFailure { + stderr_bytes, + stdout_bytes, + exit_code, + })), + None => { + unsafe { + stdout_bytes.decref(roc_host); + stderr_bytes.decref(roc_host); + } + try_cmd_output_err(cmd_output_failed_to_get_exit_code(cmd_output_io_err_other( + "Process was killed by signal", + roc_host, + ))) + } + } + } + Err(error) => try_cmd_output_err(cmd_output_failed_to_get_exit_code( + cmd_output_io_err_from_io(&error, roc_host), + )), + } +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 00000000..d04d2485 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,214 @@ +use core::mem::ManuallyDrop; + +use crate::roc_platform_abi::*; +use crate::{roc_host, roc_u8_list_from_slice}; + +// 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 = HostHttpSendRequestArg0Headers; +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() { + header.decref(roc_host); + } + args.headers.decref(roc_host); + args.method_ext.decref(roc_host); + args.uri.decref(roc_host); + } + + let request = match request_result { + Ok(request) => request, + Err(err) => return http_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)) + } + }) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..7aaa40ae --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2125 @@ +//! Roc platform host implementation for Roc's direct-symbol host ABI. + +#![allow(improper_ctypes_definitions)] + +use core::mem::ManuallyDrop; +#[cfg(unix)] +use std::ffi::CStr; +use std::ffi::{c_char, c_void, OsStr as StdOsStr, OsString}; +use std::fs; +use std::io::{self, BufRead, BufReader, Read, Write}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; + +mod cmd; +mod http; +mod roc_platform_abi; +mod sqlite; +mod tcp; + +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 name (for example `HostDirListResult`), we +// omit a local alias and rely on the `use crate::roc_platform_abi::*;` glob above. + +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; + +pub(crate) type NativeOsStr = UnixBytesOrUtf8OrWindowsU16s; +type NativeOsStrPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +type NativeOsStrTag = UnixBytesOrUtf8OrWindowsU16sTag; + +pub(crate) fn roc_u8_list_from_slice(slice: &[u8], roc_host: &RocHost) -> RocListWith { + unsafe { RocListWith::::from_slice(slice, roc_host) } +} + +#[cfg(windows)] +pub(crate) fn roc_u16_list_from_slice( + slice: &[u16], + roc_host: &RocHost, +) -> RocListWith { + unsafe { RocListWith::::from_slice(slice, 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 + } +} + +pub(crate) 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::IsADirectory => $ty { + payload: $payload { is_adirectory: [] }, + tag: $tag::IsADirectory, + }, + io::ErrorKind::NotFound => $ty { + payload: $payload { not_found: [] }, + tag: $tag::NotFound, + }, + io::ErrorKind::NotADirectory => $ty { + payload: $payload { not_adirectory: [] }, + tag: $tag::NotADirectory, + }, + 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!( + dir_io_err_from_io, + dir_io_err_other, + IOErr, + IOErrTag, + IOErrPayload +); +define_common_io_err!( + env_io_err_from_io, + env_io_err_other, + IOErr, + IOErrTag, + IOErrPayload +); +define_common_io_err!( + file_io_err_from_io, + file_io_err_other, + IOErr, + IOErrTag, + IOErrPayload +); +define_common_io_err!( + path_io_err_from_io, + path_io_err_other, + IOErr, + IOErrTag, + IOErrPayload +); +define_common_io_err!( + random_io_err_from_io, + random_io_err_other, + IOErr, + IOErrTag, + IOErrPayload +); +define_common_io_err!( + stderr_io_err_from_io, + stderr_io_err_other, + IOErr, + IOErrTag, + IOErrPayload +); +define_common_io_err!( + stdin_io_err_from_io, + stdin_io_err_other, + IOErr, + IOErrTag, + IOErrPayload +); +define_common_io_err!( + stdout_io_err_from_io, + stdout_io_err_other, + IOErr, + IOErrTag, + IOErrPayload +); + +fn try_dir_unit_ok() -> DirUnitResult { + DirUnitResult { + payload: DirUnitResultPayload { ok: [] }, + tag: DirUnitResultTag::Ok, + } +} + +fn try_dir_unit_err(error: IOErr) -> 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: IOErr) -> HostDirListResult { + HostDirListResult { + payload: HostDirListResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostDirListResultTag::Err, + } +} + +fn try_env_var_ok(value: NativeOsStr) -> HostEnvVarResult { + HostEnvVarResult { + payload: HostEnvVarResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostEnvVarResultTag::Ok, + } +} + +fn try_env_var_err(error: EnvErrOrVarNotFound) -> HostEnvVarResult { + HostEnvVarResult { + payload: HostEnvVarResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostEnvVarResultTag::Err, + } +} + +fn env_var_not_found(name: NativeOsStr) -> EnvErrOrVarNotFound { + EnvErrOrVarNotFound { + payload: EnvErrOrVarNotFoundPayload { + var_not_found: ManuallyDrop::new(name), + }, + tag: EnvErrOrVarNotFoundTag::VarNotFound, + } +} + +fn env_var_env_err(error: IOErr) -> EnvErrOrVarNotFound { + EnvErrOrVarNotFound { + payload: EnvErrOrVarNotFoundPayload { + env_err: ManuallyDrop::new(error), + }, + tag: EnvErrOrVarNotFoundTag::EnvErr, + } +} + +fn try_env_cwd_ok(value: UnixBytesOrUtf8OrWindowsU16s) -> HostEnvCwdResult { + HostEnvCwdResult { + payload: HostEnvCwdResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostEnvCwdResultTag::Ok, + } +} + +fn try_env_cwd_err() -> HostEnvCwdResult { + HostEnvCwdResult { + payload: HostEnvCwdResultPayload { err: [] }, + tag: HostEnvCwdResultTag::Err, + } +} + +fn try_env_exe_path_ok(value: UnixBytesOrUtf8OrWindowsU16s) -> HostEnvExePathResult { + HostEnvExePathResult { + payload: HostEnvExePathResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: HostEnvExePathResultTag::Ok, + } +} + +fn try_env_exe_path_err() -> HostEnvExePathResult { + HostEnvExePathResult { + payload: HostEnvExePathResultPayload { err: [] }, + tag: HostEnvExePathResultTag::Err, + } +} + +fn try_env_set_cwd_ok() -> HostEnvSetCwdResult { + HostEnvSetCwdResult { + payload: HostEnvSetCwdResultPayload { ok: [] }, + tag: HostEnvSetCwdResultTag::Ok, + } +} + +fn try_env_set_cwd_err(error: IOErr) -> HostEnvSetCwdResult { + HostEnvSetCwdResult { + payload: HostEnvSetCwdResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostEnvSetCwdResultTag::Err, + } +} + +#[cfg(not(target_pointer_width = "32"))] +fn env_arch(roc_host: &RocHost) -> AARCH64OrARMOrOTHEROrX64OrX86 { + let (payload, tag) = match std::env::consts::ARCH { + "aarch64" => ( + AARCH64OrARMOrOTHEROrX64OrX86Payload { aarch64: [] }, + AARCH64OrARMOrOTHEROrX64OrX86Tag::AARCH64, + ), + "arm" => ( + AARCH64OrARMOrOTHEROrX64OrX86Payload { arm: [] }, + AARCH64OrARMOrOTHEROrX64OrX86Tag::ARM, + ), + "x86_64" => ( + AARCH64OrARMOrOTHEROrX64OrX86Payload { x64: [] }, + AARCH64OrARMOrOTHEROrX64OrX86Tag::X64, + ), + "x86" => ( + AARCH64OrARMOrOTHEROrX64OrX86Payload { x86: [] }, + AARCH64OrARMOrOTHEROrX64OrX86Tag::X86, + ), + other => ( + AARCH64OrARMOrOTHEROrX64OrX86Payload { + other: ManuallyDrop::new(RocStr::from_str(other, roc_host)), + }, + AARCH64OrARMOrOTHEROrX64OrX86Tag::OTHER, + ), + }; + AARCH64OrARMOrOTHEROrX64OrX86 { payload, tag } +} + +#[cfg(not(target_pointer_width = "32"))] +fn env_os(roc_host: &RocHost) -> LINUXOrMACOSOrOTHEROrWINDOWS { + let (payload, tag) = match std::env::consts::OS { + "linux" => ( + LINUXOrMACOSOrOTHEROrWINDOWSPayload { linux: [] }, + LINUXOrMACOSOrOTHEROrWINDOWSTag::LINUX, + ), + "macos" => ( + LINUXOrMACOSOrOTHEROrWINDOWSPayload { macos: [] }, + LINUXOrMACOSOrOTHEROrWINDOWSTag::MACOS, + ), + "windows" => ( + LINUXOrMACOSOrOTHEROrWINDOWSPayload { windows: [] }, + LINUXOrMACOSOrOTHEROrWINDOWSTag::WINDOWS, + ), + other => ( + LINUXOrMACOSOrOTHEROrWINDOWSPayload { + other: ManuallyDrop::new(RocStr::from_str(other, roc_host)), + }, + LINUXOrMACOSOrOTHEROrWINDOWSTag::OTHER, + ), + }; + LINUXOrMACOSOrOTHEROrWINDOWS { payload, tag } +} + +#[cfg(target_pointer_width = "32")] +fn env_arch(roc_host: &RocHost) -> AARCH64OrARMOrOTHEROrX64OrX86 { + let (tag, other) = match std::env::consts::ARCH { + "aarch64" => (AARCH64OrARMOrOTHEROrX64OrX86Tag::AARCH64, None), + "arm" => (AARCH64OrARMOrOTHEROrX64OrX86Tag::ARM, None), + "x86_64" => (AARCH64OrARMOrOTHEROrX64OrX86Tag::X64, None), + "x86" => (AARCH64OrARMOrOTHEROrX64OrX86Tag::X86, None), + value => (AARCH64OrARMOrOTHEROrX64OrX86Tag::OTHER, Some(value)), + }; + let mut result = AARCH64OrARMOrOTHEROrX64OrX86 { + _payload_alignment: [], + payload: [0; 12], + tag, + }; + if let Some(value) = other { + unsafe { + core::ptr::write( + result.payload.as_mut_ptr().cast::(), + RocStr::from_str(value, roc_host), + ); + } + } + result +} + +#[cfg(target_pointer_width = "32")] +fn env_os(roc_host: &RocHost) -> LINUXOrMACOSOrOTHEROrWINDOWS { + let (tag, other) = match std::env::consts::OS { + "linux" => (LINUXOrMACOSOrOTHEROrWINDOWSTag::LINUX, None), + "macos" => (LINUXOrMACOSOrOTHEROrWINDOWSTag::MACOS, None), + "windows" => (LINUXOrMACOSOrOTHEROrWINDOWSTag::WINDOWS, None), + value => (LINUXOrMACOSOrOTHEROrWINDOWSTag::OTHER, Some(value)), + }; + let mut result = LINUXOrMACOSOrOTHEROrWINDOWS { + _payload_alignment: [], + payload: [0; 12], + tag, + }; + if let Some(value) = other { + unsafe { + core::ptr::write( + result.payload.as_mut_ptr().cast::(), + RocStr::from_str(value, roc_host), + ); + } + } + result +} + +fn try_file_bytes_ok(value: RocListWith) -> FileBytesResult { + FileBytesResult { + payload: FileBytesResultPayload { + ok: ManuallyDrop::new(value), + }, + tag: FileBytesResultTag::Ok, + } +} + +fn try_file_bytes_err(error: IOErr) -> 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: IOErr) -> 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: IOErr) -> FileReaderLineResult { + FileReaderLineResult { + payload: FileReaderLineResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileReaderLineResultTag::Err, + } +} + +fn try_file_write_bytes_ok() -> HostFileWriteBytesResult { + HostFileWriteBytesResult { + payload: HostFileWriteBytesResultPayload { ok: [] }, + tag: HostFileWriteBytesResultTag::Ok, + } +} + +fn try_file_write_bytes_err(error: IOErr) -> 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: IOErr) -> FileStrResult { + FileStrResult { + payload: FileStrResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileStrResultTag::Err, + } +} + +fn try_file_write_utf8_ok() -> HostFileWriteUtf8Result { + HostFileWriteUtf8Result { + payload: HostFileWriteUtf8ResultPayload { ok: [] }, + tag: HostFileWriteUtf8ResultTag::Ok, + } +} + +fn try_file_write_utf8_err(error: IOErr) -> HostFileWriteUtf8Result { + HostFileWriteUtf8Result { + payload: HostFileWriteUtf8ResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostFileWriteUtf8ResultTag::Err, + } +} + +fn try_file_delete_ok() -> HostFileDeleteResult { + HostFileDeleteResult { + payload: HostFileDeleteResultPayload { ok: [] }, + tag: HostFileDeleteResultTag::Ok, + } +} + +fn try_file_delete_err(error: IOErr) -> 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: IOErr) -> 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: IOErr) -> 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: IOErr) -> FileTimeResult { + FileTimeResult { + payload: FileTimeResultPayload { + err: ManuallyDrop::new(error), + }, + tag: FileTimeResultTag::Err, + } +} + +fn try_utc_now_ok(nanos: u128) -> HostUtcNowResult { + HostUtcNowResult { + payload: HostUtcNowResultPayload { + ok: ManuallyDrop::new(nanos), + }, + tag: HostUtcNowResultTag::Ok, + } +} + +fn try_utc_now_err() -> HostUtcNowResult { + HostUtcNowResult { + payload: HostUtcNowResultPayload { err: [] }, + tag: HostUtcNowResultTag::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: IOErr) -> 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: IOErr) -> 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: IOErr) -> RandomU32Result { + RandomU32Result { + payload: RandomU32ResultPayload { + err: ManuallyDrop::new(error), + }, + tag: RandomU32ResultTag::Err, + } +} + +fn try_stderr_unit_ok() -> StderrUnitResult { + StderrUnitResult { + payload: StderrUnitResultPayload { ok: [] }, + tag: StderrUnitResultTag::Ok, + } +} + +fn try_stderr_unit_err(error: IOErr) -> StderrUnitResult { + StderrUnitResult { + payload: StderrUnitResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StderrUnitResultTag::Err, + } +} + +fn try_stderr_bytes_ok() -> StderrBytesResult { + StderrBytesResult { + payload: StderrBytesResultPayload { ok: [] }, + tag: StderrBytesResultTag::Ok, + } +} + +fn try_stderr_bytes_err(error: IOErr) -> 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: IOErr) -> 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: IOErr) -> 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: IOErr) -> HostStdinReadToEndResult { + HostStdinReadToEndResult { + payload: HostStdinReadToEndResultPayload { + err: ManuallyDrop::new(error), + }, + tag: HostStdinReadToEndResultTag::Err, + } +} + +fn try_stdout_unit_ok() -> StdoutUnitResult { + StdoutUnitResult { + payload: StdoutUnitResultPayload { ok: [] }, + tag: StdoutUnitResultTag::Ok, + } +} + +fn try_stdout_unit_err(error: IOErr) -> StdoutUnitResult { + StdoutUnitResult { + payload: StdoutUnitResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StdoutUnitResultTag::Err, + } +} + +fn try_stdout_bytes_ok() -> StdoutBytesResult { + StdoutBytesResult { + payload: StdoutBytesResultPayload { ok: [] }, + tag: StdoutBytesResultTag::Ok, + } +} + +fn try_stdout_bytes_err(error: IOErr) -> StdoutBytesResult { + StdoutBytesResult { + payload: StdoutBytesResultPayload { + err: ManuallyDrop::new(error), + }, + tag: StdoutBytesResultTag::Err, + } +} + +fn unsupported_native_variant(expected: &str, got: &str) -> io::Error { + io::Error::new( + io::ErrorKind::Unsupported, + format!("expected {expected} native value on this platform, got {got}"), + ) +} + +fn path_from_native( + path: UnixBytesOrUtf8OrWindowsU16s, + roc_host: &RocHost, +) -> io::Result { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + + match path.tag { + UnixBytesOrUtf8OrWindowsU16sTag::UnixBytes => unsafe { + let bytes = ManuallyDrop::into_inner(path.payload.unix_bytes); + let path_buf = + std::path::PathBuf::from(std::ffi::OsStr::from_bytes(bytes.as_slice())); + bytes.decref(roc_host); + Ok(path_buf) + }, + UnixBytesOrUtf8OrWindowsU16sTag::Utf8 => unsafe { + let text = ManuallyDrop::into_inner(path.payload.utf8); + let path_buf = + std::path::PathBuf::from(std::ffi::OsStr::from_bytes(text.as_str().as_bytes())); + text.decref(roc_host); + Ok(path_buf) + }, + UnixBytesOrUtf8OrWindowsU16sTag::WindowsU16s => unsafe { + let u16s = ManuallyDrop::into_inner(path.payload.windows_u16s); + u16s.decref(roc_host); + Err(unsupported_native_variant("UnixBytes", "WindowsU16s")) + }, + } + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStringExt; + + match path.tag { + UnixBytesOrUtf8OrWindowsU16sTag::UnixBytes => unsafe { + let bytes = ManuallyDrop::into_inner(path.payload.unix_bytes); + bytes.decref(roc_host); + Err(unsupported_native_variant("WindowsU16s", "UnixBytes")) + }, + UnixBytesOrUtf8OrWindowsU16sTag::Utf8 => unsafe { + let text = ManuallyDrop::into_inner(path.payload.utf8); + let path_buf = std::path::PathBuf::from(OsString::from(text.as_str())); + text.decref(roc_host); + Ok(path_buf) + }, + UnixBytesOrUtf8OrWindowsU16sTag::WindowsU16s => unsafe { + let u16s = ManuallyDrop::into_inner(path.payload.windows_u16s); + let path_buf = std::path::PathBuf::from(OsString::from_wide(u16s.as_slice())); + u16s.decref(roc_host); + Ok(path_buf) + }, + } + } + + #[cfg(not(any(unix, windows)))] + { + decref_unix_bytes_or_utf8or_windows_u16s(path, roc_host); + Err(io::Error::new( + io::ErrorKind::Unsupported, + "native paths are not implemented on this platform", + )) + } +} + +pub(crate) fn os_string_from_native( + os_str: NativeOsStr, + roc_host: &RocHost, +) -> io::Result { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + + match os_str.tag { + NativeOsStrTag::UnixBytes => unsafe { + let bytes = ManuallyDrop::into_inner(os_str.payload.unix_bytes); + let value = OsString::from_vec(bytes.as_slice().to_vec()); + bytes.decref(roc_host); + Ok(value) + }, + NativeOsStrTag::Utf8 => unsafe { + let text = ManuallyDrop::into_inner(os_str.payload.utf8); + let value = OsString::from_vec(text.as_str().as_bytes().to_vec()); + text.decref(roc_host); + Ok(value) + }, + NativeOsStrTag::WindowsU16s => unsafe { + let u16s = ManuallyDrop::into_inner(os_str.payload.windows_u16s); + u16s.decref(roc_host); + Err(unsupported_native_variant("UnixBytes", "WindowsU16s")) + }, + } + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStringExt; + + match os_str.tag { + NativeOsStrTag::UnixBytes => unsafe { + let bytes = ManuallyDrop::into_inner(os_str.payload.unix_bytes); + bytes.decref(roc_host); + Err(unsupported_native_variant("WindowsU16s", "UnixBytes")) + }, + NativeOsStrTag::Utf8 => unsafe { + let text = ManuallyDrop::into_inner(os_str.payload.utf8); + let value = OsString::from(text.as_str()); + text.decref(roc_host); + Ok(value) + }, + NativeOsStrTag::WindowsU16s => unsafe { + let u16s = ManuallyDrop::into_inner(os_str.payload.windows_u16s); + let value = OsString::from_wide(u16s.as_slice()); + u16s.decref(roc_host); + Ok(value) + }, + } + } + + #[cfg(not(any(unix, windows)))] + { + decref_unix_bytes_or_utf8or_windows_u16s_type6(os_str, roc_host); + Err(io::Error::new( + io::ErrorKind::Unsupported, + "native OS strings are not implemented on this platform", + )) + } +} + +fn native_os_str_from_os_str(value: &StdOsStr, roc_host: &RocHost) -> NativeOsStr { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + + NativeOsStr { + payload: NativeOsStrPayload { + unix_bytes: ManuallyDrop::new(roc_u8_list_from_slice(value.as_bytes(), roc_host)), + }, + tag: NativeOsStrTag::UnixBytes, + } + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + + let units: Vec = value.encode_wide().collect(); + NativeOsStr { + payload: NativeOsStrPayload { + windows_u16s: ManuallyDrop::new(roc_u16_list_from_slice(&units, roc_host)), + }, + tag: NativeOsStrTag::WindowsU16s, + } + } + + #[cfg(not(any(unix, windows)))] + { + let _ = value; + NativeOsStr { + payload: NativeOsStrPayload { + utf8: ManuallyDrop::new(RocStr::empty()), + }, + tag: NativeOsStrTag::Utf8, + } + } +} + +fn validate_env_key(key: &StdOsStr) -> io::Result<()> { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + + let bytes = key.as_bytes(); + if bytes.is_empty() || bytes.contains(&0) || bytes.contains(&b'=') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "environment variable names cannot be empty or contain nul bytes or '='", + )); + } + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + + let units: Vec = key.encode_wide().collect(); + if units.is_empty() || units.contains(&0) || units.contains(&(b'=' as u16)) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "environment variable names cannot be empty or contain nul code units or '='", + )); + } + } + + Ok(()) +} + +fn native_path_from_path( + path: &std::path::Path, + roc_host: &RocHost, +) -> UnixBytesOrUtf8OrWindowsU16s { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + + UnixBytesOrUtf8OrWindowsU16s { + payload: UnixBytesOrUtf8OrWindowsU16sPayload { + unix_bytes: ManuallyDrop::new(roc_u8_list_from_slice( + path.as_os_str().as_bytes(), + roc_host, + )), + }, + tag: UnixBytesOrUtf8OrWindowsU16sTag::UnixBytes, + } + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + + let units: Vec = path.as_os_str().encode_wide().collect(); + UnixBytesOrUtf8OrWindowsU16s { + payload: UnixBytesOrUtf8OrWindowsU16sPayload { + windows_u16s: ManuallyDrop::new(roc_u16_list_from_slice(&units, roc_host)), + }, + tag: UnixBytesOrUtf8OrWindowsU16sTag::WindowsU16s, + } + } + + #[cfg(not(any(unix, windows)))] + { + UnixBytesOrUtf8OrWindowsU16s { + payload: UnixBytesOrUtf8OrWindowsU16sPayload { + unix_bytes: ManuallyDrop::new(RocListWith::::empty()), + }, + tag: UnixBytesOrUtf8OrWindowsU16sTag::UnixBytes, + } + } +} + +#[no_mangle] +pub extern "C" fn hosted_dir_create(path: UnixBytesOrUtf8OrWindowsU16s) -> DirUnitResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_dir_unit_err(dir_io_err_from_io(&error, roc_host)), + }; + match fs::create_dir(path) { + 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: UnixBytesOrUtf8OrWindowsU16s) -> DirUnitResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_dir_unit_err(dir_io_err_from_io(&error, roc_host)), + }; + match fs::create_dir_all(path) { + 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: UnixBytesOrUtf8OrWindowsU16s) -> DirUnitResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_dir_unit_err(dir_io_err_from_io(&error, roc_host)), + }; + match fs::remove_dir_all(path) { + 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: UnixBytesOrUtf8OrWindowsU16s) -> DirUnitResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_dir_unit_err(dir_io_err_from_io(&error, roc_host)), + }; + match fs::remove_dir(path) { + 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: UnixBytesOrUtf8OrWindowsU16s) -> HostDirListResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_dir_list_err(dir_io_err_from_io(&error, roc_host)), + }; + match fs::read_dir(&path) { + 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(native_path_from_path(entry.as_path(), roc_host)); + } + } + try_dir_list_ok(list) + } + Err(error) => try_dir_list_err(dir_io_err_from_io( + &normalize_dir_list_err(&path, 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(native_path_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(native_path_from_path(path.as_path(), roc_host)), + Err(_) => try_env_exe_path_err(), + } +} + +#[no_mangle] +pub extern "C" fn hosted_env_temp_dir() -> UnixBytesOrUtf8OrWindowsU16s { + let roc_host = roc_host(); + native_path_from_path(std::env::temp_dir().as_path(), roc_host) +} + +#[no_mangle] +pub extern "C" fn hosted_env_var(name: NativeOsStr) -> HostEnvVarResult { + let roc_host = roc_host(); + let key = match os_string_from_native(name, roc_host) { + Ok(key) => key, + Err(error) => { + return try_env_var_err(env_var_env_err(env_io_err_from_io(&error, roc_host))); + } + }; + + if let Err(error) = validate_env_key(key.as_os_str()) { + return try_env_var_err(env_var_env_err(env_io_err_from_io(&error, roc_host))); + } + + match std::env::var_os(&key) { + Some(value) => try_env_var_ok(native_os_str_from_os_str(value.as_os_str(), roc_host)), + None => try_env_var_err(env_var_not_found(native_os_str_from_os_str( + key.as_os_str(), + roc_host, + ))), + } +} + +#[no_mangle] +pub extern "C" fn hosted_env_platform() -> HostEnvPlatform { + let roc_host = roc_host(); + HostEnvPlatform { + arch: env_arch(roc_host), + os: env_os(roc_host), + } +} + +#[no_mangle] +pub extern "C" fn hosted_env_dict() -> RocList { + let roc_host = roc_host(); + let vars: Vec<(OsString, OsString)> = std::env::vars_os().collect(); + let list = unsafe { RocList::::allocate(vars.len(), roc_host) }; + + for (index, (name, value)) in vars.iter().enumerate() { + unsafe { + list.elements.add(index).write(HostEnvDict { + _0: native_os_str_from_os_str(name.as_os_str(), roc_host), + _1: native_os_str_from_os_str(value.as_os_str(), roc_host), + }); + } + } + + list +} + +#[no_mangle] +pub extern "C" fn hosted_env_set_cwd(path: UnixBytesOrUtf8OrWindowsU16s) -> HostEnvSetCwdResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_env_set_cwd_err(env_io_err_from_io(&error, roc_host)), + }; + + match std::env::set_current_dir(path) { + Ok(()) => try_env_set_cwd_ok(), + Err(error) => try_env_set_cwd_err(env_io_err_from_io(&error, roc_host)), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_delete(path: UnixBytesOrUtf8OrWindowsU16s) -> HostFileDeleteResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_file_delete_err(file_io_err_from_io(&error, roc_host)), + }; + match fs::remove_file(path) { + 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_hard_link( + original: UnixBytesOrUtf8OrWindowsU16s, + link: UnixBytesOrUtf8OrWindowsU16s, +) -> HostFileHardLinkResult { + let roc_host = roc_host(); + let original_path = match path_from_native(original, roc_host) { + Ok(path) => path, + Err(error) => return try_file_delete_err(file_io_err_from_io(&error, roc_host)), + }; + let link_path = match path_from_native(link, roc_host) { + Ok(path) => path, + Err(error) => return try_file_delete_err(file_io_err_from_io(&error, roc_host)), + }; + + match fs::hard_link(original_path, link_path) { + 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_rename( + from: UnixBytesOrUtf8OrWindowsU16s, + to: UnixBytesOrUtf8OrWindowsU16s, +) -> HostFileRenameResult { + let roc_host = roc_host(); + let from_path = match path_from_native(from, roc_host) { + Ok(path) => path, + Err(error) => return try_file_delete_err(file_io_err_from_io(&error, roc_host)), + }; + let to_path = match path_from_native(to, roc_host) { + Ok(path) => path, + Err(error) => return try_file_delete_err(file_io_err_from_io(&error, roc_host)), + }; + + match fs::rename(from_path, to_path) { + 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: UnixBytesOrUtf8OrWindowsU16s) -> FileBytesResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_file_bytes_err(file_io_err_from_io(&error, roc_host)), + }; + match fs::read(&path) { + 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( + &normalize_file_kind_err(&path, error), + roc_host, + )), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_read_utf8(path: UnixBytesOrUtf8OrWindowsU16s) -> FileStrResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_file_str_err(file_io_err_from_io(&error, roc_host)), + }; + match fs::read_to_string(&path) { + Ok(content) => try_file_str_ok(RocStr::from_str(&content, roc_host)), + Err(error) => try_file_str_err(file_io_err_from_io( + &normalize_file_kind_err(&path, error), + roc_host, + )), + } +} + +// ============================================================================ +// Buffered file readers +// +// The `Host.FileReader` backing `File.Reader` 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: UnixBytesOrUtf8OrWindowsU16s, + capacity: u64, +) -> FileReaderOpenResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_file_reader_err(file_io_err_from_io(&error, roc_host)), + }; + match fs::File::open(&path) { + 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( + &normalize_file_kind_err(&path, 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: UnixBytesOrUtf8OrWindowsU16s, + roc_host: &RocHost, +) -> io::Result { + fs::metadata(path_from_native(path, roc_host)?) +} + +// Windows reports ERROR_ACCESS_DENIED when a directory is opened for +// file-style reads/writes, where Unix reports EISDIR; remap so the typed +// IOErr is portable. If the metadata probe races a concurrent delete, the +// original error passes through unchanged. +#[cfg(windows)] +fn normalize_file_kind_err(path: &std::path::Path, error: io::Error) -> io::Error { + if error.kind() == io::ErrorKind::PermissionDenied { + if let Ok(metadata) = fs::metadata(path) { + if metadata.is_dir() { + return io::ErrorKind::IsADirectory.into(); + } + } + } + error +} + +#[cfg(not(windows))] +fn normalize_file_kind_err(_path: &std::path::Path, error: io::Error) -> io::Error { + error +} + +// Windows lists a directory via FindFirstFileW(path\*), whose error codes for +// non-directory paths don't always decode to NotADirectory like Unix's ENOTDIR. +#[cfg(windows)] +fn normalize_dir_list_err(path: &std::path::Path, error: io::Error) -> io::Error { + if error.kind() != io::ErrorKind::NotADirectory { + if let Ok(metadata) = fs::metadata(path) { + if !metadata.is_dir() { + return io::ErrorKind::NotADirectory.into(); + } + } + } + error +} + +#[cfg(not(windows))] +fn normalize_dir_list_err(_path: &std::path::Path, error: io::Error) -> io::Error { + error +} + +#[no_mangle] +pub extern "C" fn hosted_file_size_in_bytes(path: UnixBytesOrUtf8OrWindowsU16s) -> 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(any(unix, windows)))] +fn unsupported_file_permission_error() -> io::Error { + io::Error::new( + io::ErrorKind::Unsupported, + "file permission checks are not implemented on this platform", + ) +} + +#[cfg(unix)] +fn file_permission_bit( + path: UnixBytesOrUtf8OrWindowsU16s, + roc_host: &RocHost, + bit: u32, +) -> io::Result { + use std::os::unix::fs::PermissionsExt; + + let metadata = file_metadata(path, roc_host)?; + Ok(metadata.permissions().mode() & bit != 0) +} + +fn file_is_executable( + path: UnixBytesOrUtf8OrWindowsU16s, + roc_host: &RocHost, +) -> io::Result { + #[cfg(unix)] + { + file_permission_bit(path, roc_host, 0o111) + } + + #[cfg(windows)] + { + // Windows has no executable permission bit; approximate with the + // conventional executable extensions the shell would run. + let path = path_from_native(path, roc_host)?; + fs::metadata(&path)?; + Ok(path + .extension() + .and_then(StdOsStr::to_str) + .is_some_and(|extension| { + ["exe", "bat", "cmd", "com"] + .iter() + .any(|candidate| extension.eq_ignore_ascii_case(candidate)) + })) + } + + #[cfg(not(any(unix, windows)))] + { + let _ = path_from_native(path, roc_host); + Err(unsupported_file_permission_error()) + } +} + +fn file_is_readable( + path: UnixBytesOrUtf8OrWindowsU16s, + roc_host: &RocHost, +) -> io::Result { + #[cfg(unix)] + { + file_permission_bit(path, roc_host, 0o400) + } + + #[cfg(windows)] + { + // Windows has no read permission bit outside ACLs; anything statable + // is considered readable. + file_metadata(path, roc_host)?; + Ok(true) + } + + #[cfg(not(any(unix, windows)))] + { + let _ = path_from_native(path, roc_host); + Err(unsupported_file_permission_error()) + } +} + +fn file_is_writable( + path: UnixBytesOrUtf8OrWindowsU16s, + roc_host: &RocHost, +) -> io::Result { + #[cfg(unix)] + { + file_permission_bit(path, roc_host, 0o200) + } + + #[cfg(windows)] + { + Ok(!file_metadata(path, roc_host)?.permissions().readonly()) + } + + #[cfg(not(any(unix, windows)))] + { + let _ = path_from_native(path, roc_host); + Err(unsupported_file_permission_error()) + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_is_executable(path: UnixBytesOrUtf8OrWindowsU16s) -> FileBoolResult { + let roc_host = roc_host(); + match file_is_executable(path, roc_host) { + 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: UnixBytesOrUtf8OrWindowsU16s) -> FileBoolResult { + let roc_host = roc_host(); + match file_is_readable(path, roc_host) { + 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: UnixBytesOrUtf8OrWindowsU16s) -> FileBoolResult { + let roc_host = roc_host(); + match file_is_writable(path, roc_host) { + 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: UnixBytesOrUtf8OrWindowsU16s, + 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: UnixBytesOrUtf8OrWindowsU16s) -> 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: UnixBytesOrUtf8OrWindowsU16s) -> 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: UnixBytesOrUtf8OrWindowsU16s) -> 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: UnixBytesOrUtf8OrWindowsU16s, + bytes: RocListWith, +) -> HostFileWriteBytesResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => { + unsafe { bytes.decref(roc_host) }; + return try_file_write_bytes_err(file_io_err_from_io(&error, roc_host)); + } + }; + let result = fs::write(&path, 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( + &normalize_file_kind_err(&path, error), + roc_host, + )), + } +} + +#[no_mangle] +pub extern "C" fn hosted_file_write_utf8( + path: UnixBytesOrUtf8OrWindowsU16s, + content: RocStr, +) -> HostFileWriteUtf8Result { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => { + unsafe { content.decref(roc_host) }; + return try_file_write_utf8_err(file_io_err_from_io(&error, roc_host)); + } + }; + let content_string = content.as_str().to_owned(); + unsafe { content.decref(roc_host) }; + + match fs::write(&path, content_string) { + Ok(()) => try_file_write_utf8_ok(), + Err(error) => try_file_write_utf8_err(file_io_err_from_io( + &normalize_file_kind_err(&path, 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)) +} + +#[no_mangle] +pub extern "C" fn hosted_path_type(path: UnixBytesOrUtf8OrWindowsU16s) -> HostPathTypeResult { + let roc_host = roc_host(); + let path = match path_from_native(path, roc_host) { + Ok(path) => path, + Err(error) => return try_path_type_err(path_io_err_from_io(&error, 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(); +} + +// TODO(https://github.com/roc-lang/roc/issues/10163): revert to a bare u128 +// return once the compiler emits the clang/Rust u128 return convention on +// x86_64-windows; bare u128 returns are currently misread there. +#[no_mangle] +pub extern "C" fn hosted_utc_now() -> HostUtcNowResult { + match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => try_utc_now_ok(duration.as_nanos()), + Err(_) => try_utc_now_err(), + } +} + +#[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); +} + +#[cfg(unix)] +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_bytes(); + list.elements.offset(index).write(OsStr { + payload: OsStrPayload { + unix_bytes: ManuallyDrop::new(roc_u8_list_from_slice(arg, roc_host)), + }, + tag: OsStrTag::UnixBytes, + }); + } + } + list +} + +#[cfg(windows)] +fn build_args_list(_argc: i32, _argv: *const *const c_char, roc_host: &RocHost) -> RocList { + use std::os::windows::ffi::OsStrExt; + + let args = std::env::args_os().collect::>(); + let list = unsafe { RocList::::allocate(args.len(), roc_host) }; + for (index, arg) in args.iter().enumerate() { + let units = arg.encode_wide().collect::>(); + unsafe { + list.elements.add(index).write(OsStr { + payload: OsStrPayload { + windows_u16s: ManuallyDrop::new(roc_u16_list_from_slice(&units, roc_host)), + }, + tag: OsStrTag::WindowsU16s, + }); + } + } + list +} + +#[cfg(not(any(unix, windows)))] +fn build_args_list(_argc: i32, _argv: *const *const c_char, _roc_host: &RocHost) -> RocList { + RocList::empty() +} + +#[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..a91d26ed --- /dev/null +++ b/src/roc_platform_abi.rs @@ -0,0 +1,7532 @@ +//! 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::{fence, AtomicIsize, Ordering}; +#[cfg(not(no_roc_std_helpers))] +use std::alloc::Layout; + +/// Runtime representation of Roc's fixed-point `Dec` value. +/// +/// `num` stores the decimal value scaled by 10^18. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RocDec { + pub num: i128, +} + +const _: [(); 16] = [(); core::mem::size_of::()]; +const _: [(); 16] = [(); core::mem::align_of::()]; + +/// 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) + } +} + +#[inline] +fn checked_add_usize(left: usize, right: usize, context: &str) -> usize { + left.checked_add(right).expect(context) +} + +#[inline] +fn checked_mul_usize(left: usize, right: usize, context: &str) -> usize { + left.checked_mul(right).expect(context) +} + +#[inline] +fn shifted_capacity(capacity: usize) -> usize { + capacity.checked_shl(1).expect("Roc capacity does not fit shifted representation") +} + +/// 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 fn roc_erased_callable_payload_size(capture_size: usize) -> usize { + checked_add_usize( + ROC_ERASED_CALLABLE_CAPTURE_OFFSET, + capture_size, + "Roc erased-callable payload size overflow", + ) +} + +#[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 total = checked_add_usize( + extra_bytes, + roc_erased_callable_payload_size(capture_size), + "Roc erased-callable allocation size overflow", + ); + let base = roc_host.alloc(alignment, total) 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. +/// +/// # Safety +/// `data_ptr` must be a valid Roc box payload pointer. The caller must ensure +/// that the extra retained references are balanced by later decrefs. +pub unsafe 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); + if unsafe { (*rc).load(Ordering::Relaxed) } == 0 { + return; // REFCOUNT_STATIC_DATA + } + unsafe { + (*rc).fetch_add(amount, Ordering::Relaxed); + } +} + +/// Allocate a Roc box and return a pointer to its payload data. +/// +/// # Safety +/// The returned payload memory is uninitialized. The caller must initialize it +/// according to the Roc type before exposing it to safe APIs or Roc code. +pub unsafe 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 { checked_mul_usize(2, ptr_width, "Roc box header size overflow") } else { ptr_width }; + let header_bytes = required_space.max(payload_alignment); + let alloc_alignment = ptr_width.max(payload_alignment); + let total = checked_add_usize(header_bytes, payload_size, "Roc box allocation size overflow"); + let base = roc_host.alloc(alloc_alignment, total) as *mut u8; + let data = 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. +/// +/// # Safety +/// `data_ptr` must be a valid Roc box payload pointer owned by this reference. +pub unsafe fn decref_box(data_ptr: RocBox, roc_host: &RocHost) { + unsafe { + decref_box_with(data_ptr, core::mem::align_of::(), false, None, roc_host); + } +} + +/// Increment a boxed function closure. +/// +/// # Safety +/// `callable` must be a valid Roc erased-callable payload pointer. +pub unsafe fn incref_erased_callable(callable: RocErasedCallable, amount: isize) { + unsafe { + incref_box(callable as RocBox, amount); + } +} + +/// Decrement a boxed function closure and run its capture drop callback on final release. +/// +/// # Safety +/// `callable` must be a valid Roc erased-callable payload pointer owned by this reference. +pub unsafe fn decref_erased_callable(callable: RocErasedCallable, roc_host: &RocHost) { + unsafe { + 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. +/// +/// # Safety +/// `data_ptr` must be a valid Roc box payload pointer owned by this reference, +/// and `payload_alignment`/`payload_contains_refcounted` must match allocation. +pub unsafe 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); + if unsafe { (*rc).load(Ordering::Relaxed) } == 0 { + return; // REFCOUNT_STATIC_DATA + } + let prev = unsafe { (*rc).fetch_sub(1, Ordering::Release) }; + if prev == 1 { + fence(Ordering::Acquire); + 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`. +/// +/// # Safety +/// `data_ptr` must be a valid Roc box payload pointer that will not be used after this call. +pub unsafe 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::Acquire) == 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 { checked_mul_usize(2, ptr_width, "Roc box header size overflow") } 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`. + pub fn as_str(&self) -> &str { + core::str::from_utf8(self.as_slice()).expect("RocStr contained invalid UTF-8") + } + + /// Create a RocStr from a UTF-8 byte slice, using `roc_host` for heap allocation if needed. + pub fn from_slice(slice: &[u8], roc_host: &RocHost) -> Self { + core::str::from_utf8(slice).expect("RocStr::from_slice requires valid UTF-8"); + 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 = checked_add_usize(ptr_width, slice.len(), "RocStr allocation size overflow"); + 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: shifted_capacity(slice.len()), + 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. + /// + /// # Safety + /// `self` must own one live Roc reference. Calling this more than once for the + /// same ownership reference may double-free. + pub unsafe 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; + } + let rc = unsafe { (alloc_ptr as *mut AtomicIsize).sub(1) }; + if unsafe { (*rc).load(Ordering::Relaxed) } == 0 { + return; // REFCOUNT_STATIC_DATA — bytes are in read-only memory + } + let prev = unsafe { (*rc).fetch_sub(1, Ordering::Release) }; + if prev == 1 { + fence(Ordering::Acquire); + let ptr_width = core::mem::size_of::(); + let base = unsafe { alloc_ptr.sub(ptr_width) } as *mut c_void; + unsafe { + roc_host.dealloc(base, core::mem::align_of::()); + } + } + } + + /// Increment the reference count by `amount`. + /// + /// # Safety + /// `self` must point at a live Roc allocation. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + if self.is_small_str() { + return; + } + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return; + } + let rc = unsafe { (alloc_ptr as *mut AtomicIsize).sub(1) }; + if unsafe { (*rc).load(Ordering::Relaxed) } == 0 { + return; // REFCOUNT_STATIC_DATA + } + unsafe { + (*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::Acquire); + 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("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 { checked_mul_usize(2, ptr_width, "RocList header size overflow") } 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. + /// + /// # Safety + /// The returned element memory is uninitialized while `length` is already + /// set. The caller must initialize every element before exposing the list + /// to safe APIs, Roc code, or generated refcount helpers. + pub unsafe 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 = checked_mul_usize(length, core::mem::size_of::(), "RocList allocation element bytes overflow"); + let total = checked_add_usize(data_bytes, header_bytes, "RocList allocation size overflow"); + let base = roc_host.alloc(align, total); + let data_ptr = (base as *mut u8).add(header_bytes); + unsafe { + let rc = (data_ptr as *mut isize).sub(1); + *rc = 1; + if ELEMENTS_REFCOUNTED { + let count = (data_ptr as *mut usize).sub(2); + *count = length; + } + } + Self { + elements: data_ptr as *mut T, + length, + capacity_or_alloc_ptr: shifted_capacity(length), + } + } + + /// Create a RocList from a slice, copying elements into a new allocation. + /// + /// # Safety + /// This is a shallow copy. For element types that own Roc references, the + /// caller must ensure each copied element is already retained for the new + /// list or will not be decref'd through another owner. + pub unsafe fn from_slice(slice: &[T], roc_host: &RocHost) -> Self where T: Copy { + if slice.is_empty() { + return Self::empty(); + } + let list = unsafe { 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. + /// + /// # Safety + /// `self` must own one live Roc list reference. Calling this more than once + /// for the same ownership reference may double-free. + pub unsafe 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(); + let rc = unsafe { (alloc_ptr as *mut AtomicIsize).sub(1) }; + if unsafe { (*rc).load(Ordering::Relaxed) } == 0 { + return; // REFCOUNT_STATIC_DATA — elements are in read-only memory + } + let prev = unsafe { (*rc).fetch_sub(1, Ordering::Release) }; + if prev == 1 { + fence(Ordering::Acquire); + let base = unsafe { alloc_ptr.sub(header_bytes) } as *mut c_void; + unsafe { + roc_host.dealloc(base, align); + } + } + } + + /// Increment the reference count by `amount`. + /// + /// # Safety + /// `self` must point at a live Roc list allocation. The retained references + /// must be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + if self.elements.is_null() { + return; + } + let alloc_ptr = self.get_allocation_ptr(); + if alloc_ptr.is_null() { + return; + } + let rc = unsafe { (alloc_ptr as *mut AtomicIsize).sub(1) }; + if unsafe { (*rc).load(Ordering::Relaxed) } == 0 { + return; // REFCOUNT_STATIC_DATA + } + unsafe { + (*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::Acquire); + 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::Acquire) == 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 __AnonStruct_32ddec9aa3de7110 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct32ddec9aa3de7110 { + pub args: RocList, + pub envs: RocList, + pub program: UnixBytesOrUtf8OrWindowsU16s, + pub clear_envs: bool, +} + +/// Element type for __AnonStruct_32ddec9aa3de7110 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct32ddec9aa3de7110 { + pub args: RocList, + pub envs: RocList, + pub program: UnixBytesOrUtf8OrWindowsU16s, + pub clear_envs: bool, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 88, "AnonStruct32ddec9aa3de7110 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct32ddec9aa3de7110 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 44, "AnonStruct32ddec9aa3de7110 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AnonStruct32ddec9aa3de7110 alignment mismatch"); + +/// Element type for __AnonStruct_3f89ee1e14924626 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct3f89ee1e14924626 { + pub stderr_bytes: RocListWith, + pub stdout_bytes: RocListWith, + pub exit_code: i32, +} + +/// Element type for __AnonStruct_3f89ee1e14924626 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct3f89ee1e14924626 { + pub stderr_bytes: RocListWith, + pub stdout_bytes: RocListWith, + pub exit_code: i32, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 56, "AnonStruct3f89ee1e14924626 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct3f89ee1e14924626 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 28, "AnonStruct3f89ee1e14924626 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AnonStruct3f89ee1e14924626 alignment mismatch"); + +/// Element type for __AnonStruct_3e7554e024207e25 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct3e7554e024207e25 { + pub stderr_bytes: RocListWith, + pub stdout_bytes: RocListWith, +} + +/// Element type for __AnonStruct_3e7554e024207e25 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct3e7554e024207e25 { + pub stderr_bytes: RocListWith, + pub stdout_bytes: RocListWith, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 48, "AnonStruct3e7554e024207e25 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct3e7554e024207e25 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "AnonStruct3e7554e024207e25 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AnonStruct3e7554e024207e25 alignment mismatch"); + +/// Element type for __AnonStruct_be6bcbc15f8a1360 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStructBe6bcbc15f8a1360 { + pub body: RocListWith, + pub headers: RocList, + pub status: u16, +} + +/// Element type for __AnonStruct_be6bcbc15f8a1360 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStructBe6bcbc15f8a1360 { + pub body: RocListWith, + pub headers: RocList, + pub status: u16, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 56, "AnonStructBe6bcbc15f8a1360 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStructBe6bcbc15f8a1360 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 28, "AnonStructBe6bcbc15f8a1360 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AnonStructBe6bcbc15f8a1360 alignment mismatch"); + +/// Element type for __AnonStruct_77eaba63dfee299d +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct77eaba63dfee299d { + pub _0: RocStr, + pub _1: RocStr, +} + +/// Element type for __AnonStruct_77eaba63dfee299d +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct77eaba63dfee299d { + pub _0: RocStr, + pub _1: RocStr, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 48, "AnonStruct77eaba63dfee299d size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct77eaba63dfee299d alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "AnonStruct77eaba63dfee299d size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AnonStruct77eaba63dfee299d alignment mismatch"); + +/// Element type for __AnonStruct_8bbc5017d7a8cb36 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct8bbc5017d7a8cb36 { + pub timeout_ms: u64, + pub body: RocListWith, + pub headers: RocList, + pub method_ext: RocStr, + pub uri: RocStr, + pub method: u8, +} + +/// Element type for __AnonStruct_8bbc5017d7a8cb36 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct8bbc5017d7a8cb36 { + pub timeout_ms: u64, + pub body: RocListWith, + pub headers: RocList, + pub method_ext: RocStr, + pub uri: RocStr, + pub method: u8, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 112, "AnonStruct8bbc5017d7a8cb36 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct8bbc5017d7a8cb36 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 64, "AnonStruct8bbc5017d7a8cb36 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct8bbc5017d7a8cb36 alignment mismatch"); + +/// Element type for __AnonStruct_8dfa7f17f2083a52 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct8dfa7f17f2083a52 { + pub is_dir: bool, + pub is_file: bool, + pub is_sym_link: bool, +} + +/// Element type for __AnonStruct_8dfa7f17f2083a52 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct8dfa7f17f2083a52 { + pub is_dir: bool, + pub is_file: bool, + pub is_sym_link: bool, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 3, "AnonStruct8dfa7f17f2083a52 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 1, "AnonStruct8dfa7f17f2083a52 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 3, "AnonStruct8dfa7f17f2083a52 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 1, "AnonStruct8dfa7f17f2083a52 alignment mismatch"); + +/// Element type for __AnonStruct_22cf486058afc711 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct22cf486058afc711 { + pub code: i64, + pub message: RocStr, +} + +/// Element type for __AnonStruct_22cf486058afc711 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct22cf486058afc711 { + pub code: i64, + pub message: RocStr, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "AnonStruct22cf486058afc711 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct22cf486058afc711 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "AnonStruct22cf486058afc711 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct22cf486058afc711 alignment mismatch"); + +/// Element type for __AnonStruct_2782504baf739389 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct2782504baf739389 { + pub value: BytesOrIntegerOrNullOrRealOrString, + pub name: RocStr, +} + +/// Element type for __AnonStruct_2782504baf739389 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct2782504baf739389 { + pub value: BytesOrIntegerOrNullOrRealOrString, + pub name: RocStr, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 56, "AnonStruct2782504baf739389 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct2782504baf739389 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "AnonStruct2782504baf739389 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct2782504baf739389 alignment mismatch"); + +/// Element type for __AnonStruct_bca0d23b5d625934 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStructBca0d23b5d625934 { + pub arch: AARCH64OrARMOrOTHEROrX64OrX86, + pub os: LINUXOrMACOSOrOTHEROrWINDOWS, +} + +/// Element type for __AnonStruct_bca0d23b5d625934 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStructBca0d23b5d625934 { + pub arch: AARCH64OrARMOrOTHEROrX64OrX86, + pub os: LINUXOrMACOSOrOTHEROrWINDOWS, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 64, "AnonStructBca0d23b5d625934 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStructBca0d23b5d625934 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "AnonStructBca0d23b5d625934 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AnonStructBca0d23b5d625934 alignment mismatch"); + +/// Element type for __AnonStruct_69eee2ff6c448fed +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct69eee2ff6c448fed { + pub _0: UnixBytesOrUtf8OrWindowsU16s, + pub _1: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Element type for __AnonStruct_69eee2ff6c448fed +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStruct69eee2ff6c448fed { + pub _0: UnixBytesOrUtf8OrWindowsU16s, + pub _1: UnixBytesOrUtf8OrWindowsU16s, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 64, "AnonStruct69eee2ff6c448fed size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct69eee2ff6c448fed alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "AnonStruct69eee2ff6c448fed size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AnonStruct69eee2ff6c448fed alignment mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostCmdExecExitCodeResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostCmdExecExitCodeResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostCmdExecExitCodeResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecExitCodeResult { + pub _payload_alignment: [HostCmdExecExitCodeResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostCmdExecExitCodeResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecExitCodeResult { + pub payload: HostCmdExecExitCodeResultPayload, + pub tag: HostCmdExecExitCodeResultTag, +} + +impl HostCmdExecExitCodeResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> HostIOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const HostIOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> HostIOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> i32 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const i32) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> i32 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostCmdExecExitCodeResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostCmdExecExitCodeResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostCmdExecExitCodeResult, tag) == 32, "HostCmdExecExitCodeResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostCmdExecExitCodeResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostCmdExecExitCodeResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostCmdExecExitCodeResult, tag) == 16, "HostCmdExecExitCodeResult tag offset mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostIOErrTag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + IsADirectory = 3, + NotADirectory = 4, + NotFound = 5, + Other = 6, + OutOfMemory = 7, + PermissionDenied = 8, + Unsupported = 9, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostIOErrPayload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub is_adirectory: [u8; 0], + pub not_adirectory: [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], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostIOErrPayloadAlignment; + +/// Tag union: IOErr +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostIOErr { + pub _payload_alignment: [HostIOErrPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: HostIOErrTag, +} + +/// Tag union: IOErr +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostIOErr { + pub payload: HostIOErrPayload, + pub tag: HostIOErrTag, +} + +impl HostIOErr { + #[cfg(target_pointer_width = "32")] + pub fn payload_other(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_other(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.other) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "HostIOErr size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostIOErr alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostIOErr, tag) == 24, "HostIOErr tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "HostIOErr size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostIOErr alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostIOErr, tag) == 12, "HostIOErr tag offset mismatch"); + +/// Tag discriminant for UnixBytesOrUtf8OrWindowsU16s. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnixBytesOrUtf8OrWindowsU16sTag { + UnixBytes = 0, + Utf8 = 1, + WindowsU16s = 2, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union UnixBytesOrUtf8OrWindowsU16sPayload { + pub unix_bytes: core::mem::ManuallyDrop>, + pub utf8: core::mem::ManuallyDrop, + pub windows_u16s: core::mem::ManuallyDrop>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct UnixBytesOrUtf8OrWindowsU16sPayloadAlignment; + +/// Tag union: UnixBytesOrUtf8OrWindowsU16s +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct UnixBytesOrUtf8OrWindowsU16s { + pub _payload_alignment: [UnixBytesOrUtf8OrWindowsU16sPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: UnixBytesOrUtf8OrWindowsU16sTag, +} + +/// Tag union: UnixBytesOrUtf8OrWindowsU16s +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct UnixBytesOrUtf8OrWindowsU16s { + pub payload: UnixBytesOrUtf8OrWindowsU16sPayload, + pub tag: UnixBytesOrUtf8OrWindowsU16sTag, +} + +impl UnixBytesOrUtf8OrWindowsU16s { + #[cfg(target_pointer_width = "32")] + pub fn payload_unix_bytes(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_unix_bytes(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.unix_bytes) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_utf8(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_utf8(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.utf8) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_windows_u16s(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_windows_u16s(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.windows_u16s) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "UnixBytesOrUtf8OrWindowsU16s size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "UnixBytesOrUtf8OrWindowsU16s alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(UnixBytesOrUtf8OrWindowsU16s, tag) == 24, "UnixBytesOrUtf8OrWindowsU16s tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "UnixBytesOrUtf8OrWindowsU16s size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "UnixBytesOrUtf8OrWindowsU16s alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(UnixBytesOrUtf8OrWindowsU16s, tag) == 12, "UnixBytesOrUtf8OrWindowsU16s tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostCmdExecOutputResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostCmdExecOutputResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostCmdExecOutputResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecOutputResult { + pub _payload_alignment: [HostCmdExecOutputResultPayloadAlignment; 0], + pub payload: [u8; 32], + pub tag: HostCmdExecOutputResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecOutputResult { + pub payload: HostCmdExecOutputResultPayload, + pub tag: HostCmdExecOutputResultTag, +} + +impl HostCmdExecOutputResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> FailedToGetExitCodeOrNonZeroExitCode { + unsafe { core::ptr::read(self.payload.as_ptr() as *const FailedToGetExitCodeOrNonZeroExitCode) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> FailedToGetExitCodeOrNonZeroExitCode { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> AnonStruct3e7554e024207e25 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct3e7554e024207e25) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> AnonStruct3e7554e024207e25 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 72, "HostCmdExecOutputResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostCmdExecOutputResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostCmdExecOutputResult, tag) == 64, "HostCmdExecOutputResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 36, "HostCmdExecOutputResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostCmdExecOutputResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostCmdExecOutputResult, tag) == 32, "HostCmdExecOutputResult tag offset mismatch"); + +/// Tag discriminant for FailedToGetExitCodeOrNonZeroExitCode. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FailedToGetExitCodeOrNonZeroExitCodeTag { + FailedToGetExitCode = 0, + NonZeroExitCode = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union FailedToGetExitCodeOrNonZeroExitCodePayload { + pub failed_to_get_exit_code: core::mem::ManuallyDrop, + pub non_zero_exit_code: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct FailedToGetExitCodeOrNonZeroExitCodePayloadAlignment; + +/// Tag union: FailedToGetExitCodeOrNonZeroExitCode +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FailedToGetExitCodeOrNonZeroExitCode { + pub _payload_alignment: [FailedToGetExitCodeOrNonZeroExitCodePayloadAlignment; 0], + pub payload: [u8; 28], + pub tag: FailedToGetExitCodeOrNonZeroExitCodeTag, +} + +/// Tag union: FailedToGetExitCodeOrNonZeroExitCode +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FailedToGetExitCodeOrNonZeroExitCode { + pub payload: FailedToGetExitCodeOrNonZeroExitCodePayload, + pub tag: FailedToGetExitCodeOrNonZeroExitCodeTag, +} + +impl FailedToGetExitCodeOrNonZeroExitCode { + #[cfg(target_pointer_width = "32")] + pub fn payload_failed_to_get_exit_code(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_failed_to_get_exit_code(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.failed_to_get_exit_code) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_non_zero_exit_code(&self) -> AnonStruct3f89ee1e14924626 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct3f89ee1e14924626) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_non_zero_exit_code(&self) -> AnonStruct3f89ee1e14924626 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.non_zero_exit_code) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 64, "FailedToGetExitCodeOrNonZeroExitCode size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "FailedToGetExitCodeOrNonZeroExitCode alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(FailedToGetExitCodeOrNonZeroExitCode, tag) == 56, "FailedToGetExitCodeOrNonZeroExitCode tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "FailedToGetExitCodeOrNonZeroExitCode size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "FailedToGetExitCodeOrNonZeroExitCode alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(FailedToGetExitCodeOrNonZeroExitCode, tag) == 28, "FailedToGetExitCodeOrNonZeroExitCode tag offset mismatch"); + +/// Tag discriminant for IOErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IOErrTag { + AlreadyExists = 0, + BrokenPipe = 1, + Interrupted = 2, + IsADirectory = 3, + NotADirectory = 4, + NotFound = 5, + Other = 6, + OutOfMemory = 7, + PermissionDenied = 8, + Unsupported = 9, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union IOErrPayload { + pub already_exists: [u8; 0], + pub broken_pipe: [u8; 0], + pub interrupted: [u8; 0], + pub is_adirectory: [u8; 0], + pub not_adirectory: [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], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct IOErrPayloadAlignment; + +/// Tag union: IOErr +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErr { + pub _payload_alignment: [IOErrPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: IOErrTag, +} + +/// Tag union: IOErr +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IOErr { + pub payload: IOErrPayload, + pub tag: IOErrTag, +} + +impl IOErr { + #[cfg(target_pointer_width = "32")] + pub fn payload_other(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_other(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.other) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "IOErr size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "IOErr alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(IOErr, tag) == 24, "IOErr tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "IOErr size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "IOErr alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(IOErr, tag) == 12, "IOErr tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostDirCreateResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostDirCreateResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostDirCreateResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirCreateResult { + pub _payload_alignment: [HostDirCreateResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostDirCreateResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirCreateResult { + pub payload: HostDirCreateResultPayload, + pub tag: HostDirCreateResultTag, +} + +impl HostDirCreateResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostDirCreateResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostDirCreateResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostDirCreateResult, tag) == 32, "HostDirCreateResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostDirCreateResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostDirCreateResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostDirCreateResult, tag) == 16, "HostDirCreateResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostDirListResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostDirListResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostDirListResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirListResult { + pub _payload_alignment: [HostDirListResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostDirListResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirListResult { + pub payload: HostDirListResultPayload, + pub tag: HostDirListResultTag, +} + +impl HostDirListResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> RocList { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocList) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> RocList { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostDirListResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostDirListResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostDirListResult, tag) == 32, "HostDirListResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostDirListResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostDirListResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostDirListResult, tag) == 16, "HostDirListResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostEnvVarResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostEnvVarResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostEnvVarResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvVarResult { + pub _payload_alignment: [HostEnvVarResultPayloadAlignment; 0], + pub payload: [u8; 20], + pub tag: HostEnvVarResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvVarResult { + pub payload: HostEnvVarResultPayload, + pub tag: HostEnvVarResultTag, +} + +impl HostEnvVarResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> EnvErrOrVarNotFound { + unsafe { core::ptr::read(self.payload.as_ptr() as *const EnvErrOrVarNotFound) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> EnvErrOrVarNotFound { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> UnixBytesOrUtf8OrWindowsU16s { + unsafe { core::ptr::read(self.payload.as_ptr() as *const UnixBytesOrUtf8OrWindowsU16s) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> UnixBytesOrUtf8OrWindowsU16s { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 48, "HostEnvVarResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostEnvVarResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostEnvVarResult, tag) == 40, "HostEnvVarResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "HostEnvVarResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostEnvVarResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostEnvVarResult, tag) == 20, "HostEnvVarResult tag offset mismatch"); + +/// Tag discriminant for EnvErrOrVarNotFound. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EnvErrOrVarNotFoundTag { + EnvErr = 0, + VarNotFound = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union EnvErrOrVarNotFoundPayload { + pub env_err: core::mem::ManuallyDrop, + pub var_not_found: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct EnvErrOrVarNotFoundPayloadAlignment; + +/// Tag union: EnvErrOrVarNotFound +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EnvErrOrVarNotFound { + pub _payload_alignment: [EnvErrOrVarNotFoundPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: EnvErrOrVarNotFoundTag, +} + +/// Tag union: EnvErrOrVarNotFound +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EnvErrOrVarNotFound { + pub payload: EnvErrOrVarNotFoundPayload, + pub tag: EnvErrOrVarNotFoundTag, +} + +impl EnvErrOrVarNotFound { + #[cfg(target_pointer_width = "32")] + pub fn payload_env_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_env_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.env_err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_var_not_found(&self) -> UnixBytesOrUtf8OrWindowsU16s { + unsafe { core::ptr::read(self.payload.as_ptr() as *const UnixBytesOrUtf8OrWindowsU16s) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_var_not_found(&self) -> UnixBytesOrUtf8OrWindowsU16s { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.var_not_found) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "EnvErrOrVarNotFound size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "EnvErrOrVarNotFound alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(EnvErrOrVarNotFound, tag) == 32, "EnvErrOrVarNotFound tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "EnvErrOrVarNotFound size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "EnvErrOrVarNotFound alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(EnvErrOrVarNotFound, tag) == 16, "EnvErrOrVarNotFound tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostEnvCwdResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostEnvCwdResultPayload { + pub err: [u8; 0], + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostEnvCwdResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvCwdResult { + pub _payload_alignment: [HostEnvCwdResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostEnvCwdResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvCwdResult { + pub payload: HostEnvCwdResultPayload, + pub tag: HostEnvCwdResultTag, +} + +impl HostEnvCwdResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> UnixBytesOrUtf8OrWindowsU16s { + unsafe { core::ptr::read(self.payload.as_ptr() as *const UnixBytesOrUtf8OrWindowsU16s) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> UnixBytesOrUtf8OrWindowsU16s { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostEnvCwdResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostEnvCwdResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostEnvCwdResult, tag) == 32, "HostEnvCwdResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostEnvCwdResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostEnvCwdResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostEnvCwdResult, tag) == 16, "HostEnvCwdResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostEnvExePathResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostEnvExePathResultPayload { + pub err: [u8; 0], + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostEnvExePathResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvExePathResult { + pub _payload_alignment: [HostEnvExePathResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostEnvExePathResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvExePathResult { + pub payload: HostEnvExePathResultPayload, + pub tag: HostEnvExePathResultTag, +} + +impl HostEnvExePathResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> UnixBytesOrUtf8OrWindowsU16s { + unsafe { core::ptr::read(self.payload.as_ptr() as *const UnixBytesOrUtf8OrWindowsU16s) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> UnixBytesOrUtf8OrWindowsU16s { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostEnvExePathResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostEnvExePathResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostEnvExePathResult, tag) == 32, "HostEnvExePathResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostEnvExePathResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostEnvExePathResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostEnvExePathResult, tag) == 16, "HostEnvExePathResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostFileReadBytesResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostFileReadBytesResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostFileReadBytesResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileReadBytesResult { + pub _payload_alignment: [HostFileReadBytesResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostFileReadBytesResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileReadBytesResult { + pub payload: HostFileReadBytesResultPayload, + pub tag: HostFileReadBytesResultTag, +} + +impl HostFileReadBytesResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostFileReadBytesResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostFileReadBytesResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostFileReadBytesResult, tag) == 32, "HostFileReadBytesResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostFileReadBytesResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostFileReadBytesResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostFileReadBytesResult, tag) == 16, "HostFileReadBytesResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostFileDeleteResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostFileDeleteResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostFileDeleteResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileDeleteResult { + pub _payload_alignment: [HostFileDeleteResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostFileDeleteResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileDeleteResult { + pub payload: HostFileDeleteResultPayload, + pub tag: HostFileDeleteResultTag, +} + +impl HostFileDeleteResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostFileDeleteResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostFileDeleteResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostFileDeleteResult, tag) == 32, "HostFileDeleteResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostFileDeleteResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostFileDeleteResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostFileDeleteResult, tag) == 16, "HostFileDeleteResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostFileReadUtf8ResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostFileReadUtf8ResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostFileReadUtf8ResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileReadUtf8Result { + pub _payload_alignment: [HostFileReadUtf8ResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostFileReadUtf8ResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileReadUtf8Result { + pub payload: HostFileReadUtf8ResultPayload, + pub tag: HostFileReadUtf8ResultTag, +} + +impl HostFileReadUtf8Result { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostFileReadUtf8Result size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostFileReadUtf8Result alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostFileReadUtf8Result, tag) == 32, "HostFileReadUtf8Result tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostFileReadUtf8Result size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostFileReadUtf8Result alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostFileReadUtf8Result, tag) == 16, "HostFileReadUtf8Result tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostFileOpenReaderResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostFileOpenReaderResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<*mut u64>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostFileOpenReaderResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileOpenReaderResult { + pub _payload_alignment: [HostFileOpenReaderResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostFileOpenReaderResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileOpenReaderResult { + pub payload: HostFileOpenReaderResultPayload, + pub tag: HostFileOpenReaderResultTag, +} + +impl HostFileOpenReaderResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> *mut u64 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const *mut u64) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> *mut u64 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostFileOpenReaderResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostFileOpenReaderResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostFileOpenReaderResult, tag) == 32, "HostFileOpenReaderResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostFileOpenReaderResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostFileOpenReaderResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostFileOpenReaderResult, tag) == 16, "HostFileOpenReaderResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostFileSizeInBytesResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostFileSizeInBytesResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(8))] +#[derive(Clone, Copy)] +pub struct HostFileSizeInBytesResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileSizeInBytesResult { + pub _payload_alignment: [HostFileSizeInBytesResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostFileSizeInBytesResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileSizeInBytesResult { + pub payload: HostFileSizeInBytesResultPayload, + pub tag: HostFileSizeInBytesResultTag, +} + +impl HostFileSizeInBytesResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> u64 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const u64) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> u64 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostFileSizeInBytesResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostFileSizeInBytesResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostFileSizeInBytesResult, tag) == 32, "HostFileSizeInBytesResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "HostFileSizeInBytesResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "HostFileSizeInBytesResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostFileSizeInBytesResult, tag) == 16, "HostFileSizeInBytesResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostFileIsExecutableResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostFileIsExecutableResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostFileIsExecutableResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileIsExecutableResult { + pub _payload_alignment: [HostFileIsExecutableResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostFileIsExecutableResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileIsExecutableResult { + pub payload: HostFileIsExecutableResultPayload, + pub tag: HostFileIsExecutableResultTag, +} + +impl HostFileIsExecutableResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> bool { + unsafe { core::ptr::read(self.payload.as_ptr() as *const bool) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> bool { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostFileIsExecutableResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostFileIsExecutableResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostFileIsExecutableResult, tag) == 32, "HostFileIsExecutableResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostFileIsExecutableResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostFileIsExecutableResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostFileIsExecutableResult, tag) == 16, "HostFileIsExecutableResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostFileTimeAccessedResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostFileTimeAccessedResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(16))] +#[derive(Clone, Copy)] +pub struct HostFileTimeAccessedResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileTimeAccessedResult { + pub _payload_alignment: [HostFileTimeAccessedResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostFileTimeAccessedResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileTimeAccessedResult { + pub payload: HostFileTimeAccessedResultPayload, + pub tag: HostFileTimeAccessedResultTag, +} + +impl HostFileTimeAccessedResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> u128 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const u128) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> u128 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 48, "HostFileTimeAccessedResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 16, "HostFileTimeAccessedResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostFileTimeAccessedResult, tag) == 32, "HostFileTimeAccessedResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "HostFileTimeAccessedResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 16, "HostFileTimeAccessedResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostFileTimeAccessedResult, tag) == 16, "HostFileTimeAccessedResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostHttpSendRequestResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostHttpSendRequestResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostHttpSendRequestResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostHttpSendRequestResult { + pub _payload_alignment: [HostHttpSendRequestResultPayloadAlignment; 0], + pub payload: [u8; 28], + pub tag: HostHttpSendRequestResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostHttpSendRequestResult { + pub payload: HostHttpSendRequestResultPayload, + pub tag: HostHttpSendRequestResultTag, +} + +impl HostHttpSendRequestResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> BadBodyOrNetworkErrorOrOtherOrTimeout { + unsafe { core::ptr::read(self.payload.as_ptr() as *const BadBodyOrNetworkErrorOrOtherOrTimeout) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> BadBodyOrNetworkErrorOrOtherOrTimeout { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> AnonStructBe6bcbc15f8a1360 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStructBe6bcbc15f8a1360) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> AnonStructBe6bcbc15f8a1360 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 64, "HostHttpSendRequestResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostHttpSendRequestResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostHttpSendRequestResult, tag) == 56, "HostHttpSendRequestResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "HostHttpSendRequestResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostHttpSendRequestResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostHttpSendRequestResult, tag) == 28, "HostHttpSendRequestResult tag offset mismatch"); + +/// Tag discriminant for BadBodyOrNetworkErrorOrOtherOrTimeout. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BadBodyOrNetworkErrorOrOtherOrTimeoutTag { + BadBody = 0, + NetworkError = 1, + Other = 2, + Timeout = 3, +} + +#[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], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct BadBodyOrNetworkErrorOrOtherOrTimeoutPayloadAlignment; + +/// Tag union: BadBodyOrNetworkErrorOrOtherOrTimeout +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct BadBodyOrNetworkErrorOrOtherOrTimeout { + pub _payload_alignment: [BadBodyOrNetworkErrorOrOtherOrTimeoutPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: BadBodyOrNetworkErrorOrOtherOrTimeoutTag, +} + +/// Tag union: BadBodyOrNetworkErrorOrOtherOrTimeout +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct BadBodyOrNetworkErrorOrOtherOrTimeout { + pub payload: BadBodyOrNetworkErrorOrOtherOrTimeoutPayload, + pub tag: BadBodyOrNetworkErrorOrOtherOrTimeoutTag, +} + +impl BadBodyOrNetworkErrorOrOtherOrTimeout { + #[cfg(target_pointer_width = "32")] + pub fn payload_other(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_other(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.other) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "BadBodyOrNetworkErrorOrOtherOrTimeout size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "BadBodyOrNetworkErrorOrOtherOrTimeout alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(BadBodyOrNetworkErrorOrOtherOrTimeout, tag) == 24, "BadBodyOrNetworkErrorOrOtherOrTimeout tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "BadBodyOrNetworkErrorOrOtherOrTimeout size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "BadBodyOrNetworkErrorOrOtherOrTimeout alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(BadBodyOrNetworkErrorOrOtherOrTimeout, tag) == 12, "BadBodyOrNetworkErrorOrOtherOrTimeout tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostLocaleGetResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostLocaleGetResultPayload { + pub err: [u8; 0], + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostLocaleGetResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostLocaleGetResult { + pub _payload_alignment: [HostLocaleGetResultPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: HostLocaleGetResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostLocaleGetResult { + pub payload: HostLocaleGetResultPayload, + pub tag: HostLocaleGetResultTag, +} + +impl HostLocaleGetResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "HostLocaleGetResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostLocaleGetResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostLocaleGetResult, tag) == 24, "HostLocaleGetResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "HostLocaleGetResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostLocaleGetResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostLocaleGetResult, tag) == 12, "HostLocaleGetResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostPathTypeResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostPathTypeResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostPathTypeResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostPathTypeResult { + pub _payload_alignment: [HostPathTypeResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostPathTypeResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostPathTypeResult { + pub payload: HostPathTypeResultPayload, + pub tag: HostPathTypeResultTag, +} + +impl HostPathTypeResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> AnonStruct8dfa7f17f2083a52 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct8dfa7f17f2083a52) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> AnonStruct8dfa7f17f2083a52 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostPathTypeResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostPathTypeResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostPathTypeResult, tag) == 32, "HostPathTypeResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostPathTypeResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostPathTypeResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostPathTypeResult, tag) == 16, "HostPathTypeResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostRandomSeedU64ResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostRandomSeedU64ResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(8))] +#[derive(Clone, Copy)] +pub struct HostRandomSeedU64ResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostRandomSeedU64Result { + pub _payload_alignment: [HostRandomSeedU64ResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostRandomSeedU64ResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostRandomSeedU64Result { + pub payload: HostRandomSeedU64ResultPayload, + pub tag: HostRandomSeedU64ResultTag, +} + +impl HostRandomSeedU64Result { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> u64 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const u64) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> u64 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostRandomSeedU64Result size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostRandomSeedU64Result alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostRandomSeedU64Result, tag) == 32, "HostRandomSeedU64Result tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "HostRandomSeedU64Result size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "HostRandomSeedU64Result alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostRandomSeedU64Result, tag) == 16, "HostRandomSeedU64Result tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostRandomSeedU32ResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostRandomSeedU32ResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostRandomSeedU32ResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostRandomSeedU32Result { + pub _payload_alignment: [HostRandomSeedU32ResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostRandomSeedU32ResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostRandomSeedU32Result { + pub payload: HostRandomSeedU32ResultPayload, + pub tag: HostRandomSeedU32ResultTag, +} + +impl HostRandomSeedU32Result { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> u32 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const u32) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> u32 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostRandomSeedU32Result size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostRandomSeedU32Result alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostRandomSeedU32Result, tag) == 32, "HostRandomSeedU32Result tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostRandomSeedU32Result size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostRandomSeedU32Result alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostRandomSeedU32Result, tag) == 16, "HostRandomSeedU32Result tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostSqlitePrepareResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostSqlitePrepareResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<*mut u64>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(8))] +#[derive(Clone, Copy)] +pub struct HostSqlitePrepareResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqlitePrepareResult { + pub _payload_alignment: [HostSqlitePrepareResultPayloadAlignment; 0], + pub payload: [u8; 24], + pub tag: HostSqlitePrepareResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqlitePrepareResult { + pub payload: HostSqlitePrepareResultPayload, + pub tag: HostSqlitePrepareResultTag, +} + +impl HostSqlitePrepareResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> AnonStruct22cf486058afc711 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct22cf486058afc711) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> AnonStruct22cf486058afc711 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> *mut u64 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const *mut u64) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> *mut u64 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostSqlitePrepareResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostSqlitePrepareResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostSqlitePrepareResult, tag) == 32, "HostSqlitePrepareResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "HostSqlitePrepareResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "HostSqlitePrepareResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostSqlitePrepareResult, tag) == 24, "HostSqlitePrepareResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostSqliteBindResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostSqliteBindResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(8))] +#[derive(Clone, Copy)] +pub struct HostSqliteBindResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteBindResult { + pub _payload_alignment: [HostSqliteBindResultPayloadAlignment; 0], + pub payload: [u8; 24], + pub tag: HostSqliteBindResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteBindResult { + pub payload: HostSqliteBindResultPayload, + pub tag: HostSqliteBindResultTag, +} + +impl HostSqliteBindResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> AnonStruct22cf486058afc711 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct22cf486058afc711) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> AnonStruct22cf486058afc711 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostSqliteBindResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostSqliteBindResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostSqliteBindResult, tag) == 32, "HostSqliteBindResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "HostSqliteBindResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "HostSqliteBindResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostSqliteBindResult, tag) == 24, "HostSqliteBindResult tag offset 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, +} + +#[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, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(8))] +#[derive(Clone, Copy)] +pub struct BytesOrIntegerOrNullOrRealOrStringPayloadAlignment; + +/// Tag union: BytesOrIntegerOrNullOrRealOrString +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct BytesOrIntegerOrNullOrRealOrString { + pub _payload_alignment: [BytesOrIntegerOrNullOrRealOrStringPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: BytesOrIntegerOrNullOrRealOrStringTag, +} + +/// Tag union: BytesOrIntegerOrNullOrRealOrString +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct BytesOrIntegerOrNullOrRealOrString { + pub payload: BytesOrIntegerOrNullOrRealOrStringPayload, + pub tag: BytesOrIntegerOrNullOrRealOrStringTag, +} + +impl BytesOrIntegerOrNullOrRealOrString { + #[cfg(target_pointer_width = "32")] + pub fn payload_bytes(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_bytes(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.bytes) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_integer(&self) -> i64 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const i64) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_integer(&self) -> i64 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.integer) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_real(&self) -> f64 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const f64) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_real(&self) -> f64 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.real) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_string(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_string(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.string) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "BytesOrIntegerOrNullOrRealOrString size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "BytesOrIntegerOrNullOrRealOrString alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(BytesOrIntegerOrNullOrRealOrString, tag) == 24, "BytesOrIntegerOrNullOrRealOrString tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "BytesOrIntegerOrNullOrRealOrString size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "BytesOrIntegerOrNullOrRealOrString alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(BytesOrIntegerOrNullOrRealOrString, tag) == 12, "BytesOrIntegerOrNullOrRealOrString tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostSqliteColumnValueResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostSqliteColumnValueResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(8))] +#[derive(Clone, Copy)] +pub struct HostSqliteColumnValueResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteColumnValueResult { + pub _payload_alignment: [HostSqliteColumnValueResultPayloadAlignment; 0], + pub payload: [u8; 24], + pub tag: HostSqliteColumnValueResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteColumnValueResult { + pub payload: HostSqliteColumnValueResultPayload, + pub tag: HostSqliteColumnValueResultTag, +} + +impl HostSqliteColumnValueResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> AnonStruct22cf486058afc711 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct22cf486058afc711) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> AnonStruct22cf486058afc711 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> BytesOrIntegerOrNullOrRealOrString { + unsafe { core::ptr::read(self.payload.as_ptr() as *const BytesOrIntegerOrNullOrRealOrString) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> BytesOrIntegerOrNullOrRealOrString { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostSqliteColumnValueResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostSqliteColumnValueResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostSqliteColumnValueResult, tag) == 32, "HostSqliteColumnValueResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "HostSqliteColumnValueResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "HostSqliteColumnValueResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostSqliteColumnValueResult, tag) == 24, "HostSqliteColumnValueResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostSqliteStepResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostSqliteStepResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(8))] +#[derive(Clone, Copy)] +pub struct HostSqliteStepResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteStepResult { + pub _payload_alignment: [HostSqliteStepResultPayloadAlignment; 0], + pub payload: [u8; 24], + pub tag: HostSqliteStepResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostSqliteStepResult { + pub payload: HostSqliteStepResultPayload, + pub tag: HostSqliteStepResultTag, +} + +impl HostSqliteStepResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> AnonStruct22cf486058afc711 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct22cf486058afc711) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> AnonStruct22cf486058afc711 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> bool { + unsafe { core::ptr::read(self.payload.as_ptr() as *const bool) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> bool { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostSqliteStepResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostSqliteStepResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostSqliteStepResult, tag) == 32, "HostSqliteStepResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "HostSqliteStepResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "HostSqliteStepResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostSqliteStepResult, tag) == 24, "HostSqliteStepResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostStderrLineResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostStderrLineResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostStderrLineResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStderrLineResult { + pub _payload_alignment: [HostStderrLineResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostStderrLineResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStderrLineResult { + pub payload: HostStderrLineResultPayload, + pub tag: HostStderrLineResultTag, +} + +impl HostStderrLineResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostStderrLineResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostStderrLineResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostStderrLineResult, tag) == 32, "HostStderrLineResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostStderrLineResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostStderrLineResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostStderrLineResult, tag) == 16, "HostStderrLineResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostStdinLineResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostStdinLineResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostStdinLineResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdinLineResult { + pub _payload_alignment: [HostStdinLineResultPayloadAlignment; 0], + pub payload: [u8; 20], + pub tag: HostStdinLineResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdinLineResult { + pub payload: HostStdinLineResultPayload, + pub tag: HostStdinLineResultTag, +} + +impl HostStdinLineResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> EndOfFileOrStdinErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const EndOfFileOrStdinErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> EndOfFileOrStdinErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 48, "HostStdinLineResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostStdinLineResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostStdinLineResult, tag) == 40, "HostStdinLineResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "HostStdinLineResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostStdinLineResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostStdinLineResult, tag) == 20, "HostStdinLineResult tag offset mismatch"); + +/// Tag discriminant for EndOfFileOrStdinErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EndOfFileOrStdinErrTag { + EndOfFile = 0, + StdinErr = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union EndOfFileOrStdinErrPayload { + pub end_of_file: [u8; 0], + pub stdin_err: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct EndOfFileOrStdinErrPayloadAlignment; + +/// Tag union: EndOfFileOrStdinErr +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EndOfFileOrStdinErr { + pub _payload_alignment: [EndOfFileOrStdinErrPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: EndOfFileOrStdinErrTag, +} + +/// Tag union: EndOfFileOrStdinErr +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EndOfFileOrStdinErr { + pub payload: EndOfFileOrStdinErrPayload, + pub tag: EndOfFileOrStdinErrTag, +} + +impl EndOfFileOrStdinErr { + #[cfg(target_pointer_width = "32")] + pub fn payload_stdin_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_stdin_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.stdin_err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "EndOfFileOrStdinErr size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "EndOfFileOrStdinErr alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(EndOfFileOrStdinErr, tag) == 32, "EndOfFileOrStdinErr tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "EndOfFileOrStdinErr size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "EndOfFileOrStdinErr alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(EndOfFileOrStdinErr, tag) == 16, "EndOfFileOrStdinErr tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostStdinBytesResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostStdinBytesResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostStdinBytesResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdinBytesResult { + pub _payload_alignment: [HostStdinBytesResultPayloadAlignment; 0], + pub payload: [u8; 20], + pub tag: HostStdinBytesResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdinBytesResult { + pub payload: HostStdinBytesResultPayload, + pub tag: HostStdinBytesResultTag, +} + +impl HostStdinBytesResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> EndOfFileOrStdinErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const EndOfFileOrStdinErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> EndOfFileOrStdinErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 48, "HostStdinBytesResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostStdinBytesResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostStdinBytesResult, tag) == 40, "HostStdinBytesResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "HostStdinBytesResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostStdinBytesResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostStdinBytesResult, tag) == 20, "HostStdinBytesResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostStdinReadToEndResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostStdinReadToEndResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostStdinReadToEndResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdinReadToEndResult { + pub _payload_alignment: [HostStdinReadToEndResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostStdinReadToEndResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdinReadToEndResult { + pub payload: HostStdinReadToEndResultPayload, + pub tag: HostStdinReadToEndResultTag, +} + +impl HostStdinReadToEndResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostStdinReadToEndResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostStdinReadToEndResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostStdinReadToEndResult, tag) == 32, "HostStdinReadToEndResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostStdinReadToEndResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostStdinReadToEndResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostStdinReadToEndResult, tag) == 16, "HostStdinReadToEndResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostStdoutLineResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostStdoutLineResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostStdoutLineResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdoutLineResult { + pub _payload_alignment: [HostStdoutLineResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostStdoutLineResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostStdoutLineResult { + pub payload: HostStdoutLineResultPayload, + pub tag: HostStdoutLineResultTag, +} + +impl HostStdoutLineResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostStdoutLineResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostStdoutLineResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostStdoutLineResult, tag) == 32, "HostStdoutLineResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostStdoutLineResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostStdoutLineResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostStdoutLineResult, tag) == 16, "HostStdoutLineResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostTcpConnectResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostTcpConnectResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<*mut u64>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostTcpConnectResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpConnectResult { + pub _payload_alignment: [HostTcpConnectResultPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: HostTcpConnectResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpConnectResult { + pub payload: HostTcpConnectResultPayload, + pub tag: HostTcpConnectResultTag, +} + +impl HostTcpConnectResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> *mut u64 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const *mut u64) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> *mut u64 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "HostTcpConnectResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostTcpConnectResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostTcpConnectResult, tag) == 24, "HostTcpConnectResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "HostTcpConnectResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostTcpConnectResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostTcpConnectResult, tag) == 12, "HostTcpConnectResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostTcpReadExactlyResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostTcpReadExactlyResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostTcpReadExactlyResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpReadExactlyResult { + pub _payload_alignment: [HostTcpReadExactlyResultPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: HostTcpReadExactlyResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpReadExactlyResult { + pub payload: HostTcpReadExactlyResultPayload, + pub tag: HostTcpReadExactlyResultTag, +} + +impl HostTcpReadExactlyResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "HostTcpReadExactlyResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostTcpReadExactlyResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostTcpReadExactlyResult, tag) == 24, "HostTcpReadExactlyResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "HostTcpReadExactlyResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostTcpReadExactlyResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostTcpReadExactlyResult, tag) == 12, "HostTcpReadExactlyResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostTcpWriteResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostTcpWriteResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostTcpWriteResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpWriteResult { + pub _payload_alignment: [HostTcpWriteResultPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: HostTcpWriteResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpWriteResult { + pub payload: HostTcpWriteResultPayload, + pub tag: HostTcpWriteResultTag, +} + +impl HostTcpWriteResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "HostTcpWriteResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostTcpWriteResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostTcpWriteResult, tag) == 24, "HostTcpWriteResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "HostTcpWriteResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostTcpWriteResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostTcpWriteResult, tag) == 12, "HostTcpWriteResult tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostUtcNowResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostUtcNowResultPayload { + pub err: [u8; 0], + pub ok: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(16))] +#[derive(Clone, Copy)] +pub struct HostUtcNowResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostUtcNowResult { + pub _payload_alignment: [HostUtcNowResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostUtcNowResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostUtcNowResult { + pub payload: HostUtcNowResultPayload, + pub tag: HostUtcNowResultTag, +} + +impl HostUtcNowResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_ok(&self) -> u128 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const u128) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_ok(&self) -> u128 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "HostUtcNowResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 16, "HostUtcNowResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostUtcNowResult, tag) == 16, "HostUtcNowResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "HostUtcNowResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 16, "HostUtcNowResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostUtcNowResult, tag) == 16, "HostUtcNowResult tag offset mismatch"); + +/// Tag discriminant for AARCH64OrARMOrOTHEROrX64OrX86. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AARCH64OrARMOrOTHEROrX64OrX86Tag { + AARCH64 = 0, + ARM = 1, + OTHER = 2, + X64 = 3, + X86 = 4, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union AARCH64OrARMOrOTHEROrX64OrX86Payload { + pub aarch64: [u8; 0], + pub arm: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub x64: [u8; 0], + pub x86: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct AARCH64OrARMOrOTHEROrX64OrX86PayloadAlignment; + +/// Tag union: AARCH64OrARMOrOTHEROrX64OrX86 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AARCH64OrARMOrOTHEROrX64OrX86 { + pub _payload_alignment: [AARCH64OrARMOrOTHEROrX64OrX86PayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: AARCH64OrARMOrOTHEROrX64OrX86Tag, +} + +/// Tag union: AARCH64OrARMOrOTHEROrX64OrX86 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AARCH64OrARMOrOTHEROrX64OrX86 { + pub payload: AARCH64OrARMOrOTHEROrX64OrX86Payload, + pub tag: AARCH64OrARMOrOTHEROrX64OrX86Tag, +} + +impl AARCH64OrARMOrOTHEROrX64OrX86 { + #[cfg(target_pointer_width = "32")] + pub fn payload_other(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_other(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.other) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "AARCH64OrARMOrOTHEROrX64OrX86 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AARCH64OrARMOrOTHEROrX64OrX86 alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(AARCH64OrARMOrOTHEROrX64OrX86, tag) == 24, "AARCH64OrARMOrOTHEROrX64OrX86 tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "AARCH64OrARMOrOTHEROrX64OrX86 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AARCH64OrARMOrOTHEROrX64OrX86 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(AARCH64OrARMOrOTHEROrX64OrX86, tag) == 12, "AARCH64OrARMOrOTHEROrX64OrX86 tag offset mismatch"); + +/// Tag discriminant for LINUXOrMACOSOrOTHEROrWINDOWS. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LINUXOrMACOSOrOTHEROrWINDOWSTag { + LINUX = 0, + MACOS = 1, + OTHER = 2, + WINDOWS = 3, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union LINUXOrMACOSOrOTHEROrWINDOWSPayload { + pub linux: [u8; 0], + pub macos: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub windows: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct LINUXOrMACOSOrOTHEROrWINDOWSPayloadAlignment; + +/// Tag union: LINUXOrMACOSOrOTHEROrWINDOWS +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct LINUXOrMACOSOrOTHEROrWINDOWS { + pub _payload_alignment: [LINUXOrMACOSOrOTHEROrWINDOWSPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: LINUXOrMACOSOrOTHEROrWINDOWSTag, +} + +/// Tag union: LINUXOrMACOSOrOTHEROrWINDOWS +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct LINUXOrMACOSOrOTHEROrWINDOWS { + pub payload: LINUXOrMACOSOrOTHEROrWINDOWSPayload, + pub tag: LINUXOrMACOSOrOTHEROrWINDOWSTag, +} + +impl LINUXOrMACOSOrOTHEROrWINDOWS { + #[cfg(target_pointer_width = "32")] + pub fn payload_other(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_other(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.other) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "LINUXOrMACOSOrOTHEROrWINDOWS size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "LINUXOrMACOSOrOTHEROrWINDOWS alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(LINUXOrMACOSOrOTHEROrWINDOWS, tag) == 24, "LINUXOrMACOSOrOTHEROrWINDOWS tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "LINUXOrMACOSOrOTHEROrWINDOWS size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "LINUXOrMACOSOrOTHEROrWINDOWS alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(LINUXOrMACOSOrOTHEROrWINDOWS, tag) == 12, "LINUXOrMACOSOrOTHEROrWINDOWS tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostEnvSetCwdResultTag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union HostEnvSetCwdResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct HostEnvSetCwdResultPayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvSetCwdResult { + pub _payload_alignment: [HostEnvSetCwdResultPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: HostEnvSetCwdResultTag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvSetCwdResult { + pub payload: HostEnvSetCwdResultPayload, + pub tag: HostEnvSetCwdResultTag, +} + +impl HostEnvSetCwdResult { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> IOErr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "HostEnvSetCwdResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostEnvSetCwdResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostEnvSetCwdResult, tag) == 32, "HostEnvSetCwdResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "HostEnvSetCwdResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostEnvSetCwdResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostEnvSetCwdResult, tag) == 16, "HostEnvSetCwdResult tag offset mismatch"); + +/// Tag discriminant for OsStr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OsStrTag { + UnixBytes = 0, + Utf8 = 1, + WindowsU16s = 2, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union OsStrPayload { + pub unix_bytes: core::mem::ManuallyDrop>, + pub utf8: core::mem::ManuallyDrop, + pub windows_u16s: core::mem::ManuallyDrop>, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct OsStrPayloadAlignment; + +/// Tag union: OsStr +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct OsStr { + pub _payload_alignment: [OsStrPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: OsStrTag, +} + +/// Tag union: OsStr +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct OsStr { + pub payload: OsStrPayload, + pub tag: OsStrTag, +} + +impl OsStr { + #[cfg(target_pointer_width = "32")] + pub fn payload_unix_bytes(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_unix_bytes(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.unix_bytes) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_utf8(&self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_utf8(&self) -> RocStr { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.utf8) } + } + + #[cfg(target_pointer_width = "32")] + pub fn payload_windows_u16s(&self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_windows_u16s(&self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.windows_u16s) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "OsStr size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "OsStr alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(OsStr, tag) == 24, "OsStr tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "OsStr size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "OsStr alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(OsStr, tag) == 12, "OsStr tag offset mismatch"); + +/// Tag discriminant for Try. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryType205Tag { + Err = 0, + Ok = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TryType205Payload { + pub err: core::mem::ManuallyDrop, + pub ok: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct TryType205PayloadAlignment; + +/// Tag union: Try +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType205 { + pub _payload_alignment: [TryType205PayloadAlignment; 0], + pub payload: [u8; 4], + pub tag: TryType205Tag, +} + +/// Tag union: Try +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TryType205 { + pub payload: TryType205Payload, + pub tag: TryType205Tag, +} + +impl TryType205 { + #[cfg(target_pointer_width = "32")] + pub fn payload_err(&self) -> i32 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const i32) } + } + + #[cfg(not(target_pointer_width = "32"))] + pub fn payload_err(&self) -> i32 { + unsafe { core::mem::ManuallyDrop::into_inner(self.payload.err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 8, "TryType205 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 4, "TryType205 alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(TryType205, tag) == 4, "TryType205 tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 8, "TryType205 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "TryType205 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(TryType205, tag) == 4, "TryType205 tag offset mismatch"); + +/// Return type record for Host.env_platform! +/// Fields ordered by compiler-emitted ABI offsets. +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvPlatformRetRecord { + pub arch: AARCH64OrARMOrOTHEROrX64OrX86, + pub os: LINUXOrMACOSOrOTHEROrWINDOWS, +} + +/// Return type record for Host.env_platform! +/// Fields ordered by compiler-emitted ABI offsets. +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvPlatformRetRecord { + pub arch: AARCH64OrARMOrOTHEROrX64OrX86, + pub os: LINUXOrMACOSOrOTHEROrWINDOWS, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 64, "HostEnvPlatformRetRecord size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostEnvPlatformRetRecord alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 32, "HostEnvPlatformRetRecord size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostEnvPlatformRetRecord alignment mismatch"); + +/// Arguments for Host.cmd_exec_exit_code! +/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] } => Try(I32, IOErr) +/// Refcounted fields are owned by the hosted function. +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecExitCodeArgs { + pub args: RocList, + pub envs: RocList, + pub program: UnixBytesOrUtf8OrWindowsU16s, + pub clear_envs: bool, +} + +/// Arguments for Host.cmd_exec_exit_code! +/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] } => Try(I32, IOErr) +/// Refcounted fields are owned by the hosted function. +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecExitCodeArgs { + pub args: RocList, + pub envs: RocList, + pub program: UnixBytesOrUtf8OrWindowsU16s, + pub clear_envs: bool, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 88, "HostCmdExecExitCodeArgs size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostCmdExecExitCodeArgs alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 44, "HostCmdExecExitCodeArgs size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostCmdExecExitCodeArgs alignment mismatch"); + +/// Arguments for Host.cmd_exec_output! +/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] } => Try({ stderr_bytes : List(U8), stdout_bytes : List(U8) }, [FailedToGetExitCode(IOErr), NonZeroExitCode({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) })]) +/// Refcounted fields are owned by the hosted function. +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecOutputArgs { + pub args: RocList, + pub envs: RocList, + pub program: UnixBytesOrUtf8OrWindowsU16s, + pub clear_envs: bool, +} + +/// Arguments for Host.cmd_exec_output! +/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] } => Try({ stderr_bytes : List(U8), stdout_bytes : List(U8) }, [FailedToGetExitCode(IOErr), NonZeroExitCode({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) })]) +/// Refcounted fields are owned by the hosted function. +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostCmdExecOutputArgs { + pub args: RocList, + pub envs: RocList, + pub program: UnixBytesOrUtf8OrWindowsU16s, + pub clear_envs: bool, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 88, "HostCmdExecOutputArgs size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostCmdExecOutputArgs alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 44, "HostCmdExecOutputArgs size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostCmdExecOutputArgs alignment mismatch"); + +/// Arguments for Host.dir_create! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirCreateArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.dir_create_all! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirCreateAllArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.dir_delete_all! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirDeleteAllArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.dir_delete_empty! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirDeleteEmptyArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.dir_list! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), [DirErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostDirListArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.env_set_cwd! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, IOErr) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvSetCwdArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.env_var! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [EnvErr(IOErr), VarNotFound([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))])]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostEnvVarArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_delete! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileDeleteArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_hard_link! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileHardLinkArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, + pub arg1: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_is_executable! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(Bool, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileIsExecutableArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_is_readable! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(Bool, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileIsReadableArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_is_writable! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(Bool, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileIsWritableArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_open_reader! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], U64 => Try(Host.FileReader, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileOpenReaderArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, + pub arg1: u64, +} + +/// Arguments for Host.file_read_bytes! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(List(U8), [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileReadBytesArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// 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: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(Str, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileReadUtf8Args { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_rename! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileRenameArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, + pub arg1: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_size_in_bytes! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(U64, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileSizeInBytesArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_time_accessed! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(U128, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileTimeAccessedArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_time_created! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(U128, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileTimeCreatedArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_time_modified! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(U128, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileTimeModifiedArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, +} + +/// Arguments for Host.file_write_bytes! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], List(U8) => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileWriteBytesArgs { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, + pub arg1: RocListWith, +} + +/// Arguments for Host.file_write_utf8! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], Str => Try({}, [FileErr(IOErr)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostFileWriteUtf8Args { + pub arg0: UnixBytesOrUtf8OrWindowsU16s, + 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. +#[cfg(target_pointer_width = "32")] +#[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, +} + +/// 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. +#[cfg(not(target_pointer_width = "32"))] +#[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, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 112, "HostHttpSendRequestArgs size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostHttpSendRequestArgs alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 64, "HostHttpSendRequestArgs size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 8, "HostHttpSendRequestArgs alignment mismatch"); + +/// Arguments for Host.path_type! +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => 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: UnixBytesOrUtf8OrWindowsU16s, +} + +/// 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: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], 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: UnixBytesOrUtf8OrWindowsU16s, + 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, +} + +// Platform Type Aliases + +pub type HostCmdExecExitCodeArg0 = AnonStruct32ddec9aa3de7110; +pub type HostCmdExecExitCodeArg0Args = UnixBytesOrUtf8OrWindowsU16s; +pub type HostCmdExecExitCodeArg0ArgsPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostCmdExecExitCodeArg0ArgsTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostCmdExecExitCodeArg0Envs = UnixBytesOrUtf8OrWindowsU16s; +pub type HostCmdExecExitCodeArg0EnvsPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostCmdExecExitCodeArg0EnvsTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostCmdExecOutputArg0 = AnonStruct32ddec9aa3de7110; +pub type HostCmdExecOutputArg0Args = UnixBytesOrUtf8OrWindowsU16s; +pub type HostCmdExecOutputArg0ArgsPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostCmdExecOutputArg0ArgsTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostCmdExecOutputArg0Envs = UnixBytesOrUtf8OrWindowsU16s; +pub type HostCmdExecOutputArg0EnvsPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostCmdExecOutputArg0EnvsTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostCmdExecOutputErr = FailedToGetExitCodeOrNonZeroExitCode; +pub type HostCmdExecOutputErrPayload = FailedToGetExitCodeOrNonZeroExitCodePayload; +pub type HostCmdExecOutputErrTag = FailedToGetExitCodeOrNonZeroExitCodeTag; +pub type HostCmdExecOutputErrNonZeroExitCode = AnonStruct3f89ee1e14924626; +pub type HostCmdExecOutputOk = AnonStruct3e7554e024207e25; +pub type FailedToGetExitCodeOrNonZeroExitCodeNonZeroExitCode = AnonStruct3f89ee1e14924626; +pub type HostDirCreateAllResult = HostDirCreateResult; +pub type HostDirCreateAllResultPayload = HostDirCreateResultPayload; +pub type HostDirCreateAllResultTag = HostDirCreateResultTag; +pub type HostDirDeleteAllResult = HostDirCreateResult; +pub type HostDirDeleteAllResultPayload = HostDirCreateResultPayload; +pub type HostDirDeleteAllResultTag = HostDirCreateResultTag; +pub type HostDirDeleteEmptyResult = HostDirCreateResult; +pub type HostDirDeleteEmptyResultPayload = HostDirCreateResultPayload; +pub type HostDirDeleteEmptyResultTag = HostDirCreateResultTag; +pub type HostDirListOk = UnixBytesOrUtf8OrWindowsU16s; +pub type HostDirListOkPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostDirListOkTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostEnvCwdOk = UnixBytesOrUtf8OrWindowsU16s; +pub type HostEnvCwdOkPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostEnvCwdOkTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostEnvDict = AnonStruct69eee2ff6c448fed; +pub type HostEnvExePathOk = UnixBytesOrUtf8OrWindowsU16s; +pub type HostEnvExePathOkPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostEnvExePathOkTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostEnvPlatform = AnonStructBca0d23b5d625934; +pub type HostEnvVarErr = EnvErrOrVarNotFound; +pub type HostEnvVarErrPayload = EnvErrOrVarNotFoundPayload; +pub type HostEnvVarErrTag = EnvErrOrVarNotFoundTag; +pub type HostEnvVarErrVarNotFound = UnixBytesOrUtf8OrWindowsU16s; +pub type HostEnvVarErrVarNotFoundPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostEnvVarErrVarNotFoundTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostEnvVarOk = UnixBytesOrUtf8OrWindowsU16s; +pub type HostEnvVarOkPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type HostEnvVarOkTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type EnvErrOrVarNotFoundVarNotFound = UnixBytesOrUtf8OrWindowsU16s; +pub type EnvErrOrVarNotFoundVarNotFoundPayload = UnixBytesOrUtf8OrWindowsU16sPayload; +pub type EnvErrOrVarNotFoundVarNotFoundTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type HostFileHardLinkResult = HostFileDeleteResult; +pub type HostFileHardLinkResultPayload = HostFileDeleteResultPayload; +pub type HostFileHardLinkResultTag = HostFileDeleteResultTag; +pub type HostFileIsReadableResult = HostFileIsExecutableResult; +pub type HostFileIsReadableResultPayload = HostFileIsExecutableResultPayload; +pub type HostFileIsReadableResultTag = HostFileIsExecutableResultTag; +pub type HostFileIsWritableResult = HostFileIsExecutableResult; +pub type HostFileIsWritableResultPayload = HostFileIsExecutableResultPayload; +pub type HostFileIsWritableResultTag = HostFileIsExecutableResultTag; +pub type HostFileReadLineResult = HostFileReadBytesResult; +pub type HostFileReadLineResultPayload = HostFileReadBytesResultPayload; +pub type HostFileReadLineResultTag = HostFileReadBytesResultTag; +pub type HostFileRenameResult = HostFileDeleteResult; +pub type HostFileRenameResultPayload = HostFileDeleteResultPayload; +pub type HostFileRenameResultTag = HostFileDeleteResultTag; +pub type HostFileTimeCreatedResult = HostFileTimeAccessedResult; +pub type HostFileTimeCreatedResultPayload = HostFileTimeAccessedResultPayload; +pub type HostFileTimeCreatedResultTag = HostFileTimeAccessedResultTag; +pub type HostFileTimeModifiedResult = HostFileTimeAccessedResult; +pub type HostFileTimeModifiedResultPayload = HostFileTimeAccessedResultPayload; +pub type HostFileTimeModifiedResultTag = HostFileTimeAccessedResultTag; +pub type HostFileWriteBytesResult = HostFileDeleteResult; +pub type HostFileWriteBytesResultPayload = HostFileDeleteResultPayload; +pub type HostFileWriteBytesResultTag = HostFileDeleteResultTag; +pub type HostFileWriteUtf8Result = HostFileDeleteResult; +pub type HostFileWriteUtf8ResultPayload = HostFileDeleteResultPayload; +pub type HostFileWriteUtf8ResultTag = HostFileDeleteResultTag; +pub type HostHttpSendRequestArg0 = AnonStruct8bbc5017d7a8cb36; +pub type HostHttpSendRequestArg0Headers = AnonStruct77eaba63dfee299d; +pub type HostHttpSendRequestErr = BadBodyOrNetworkErrorOrOtherOrTimeout; +pub type HostHttpSendRequestErrPayload = BadBodyOrNetworkErrorOrOtherOrTimeoutPayload; +pub type HostHttpSendRequestErrTag = BadBodyOrNetworkErrorOrOtherOrTimeoutTag; +pub type HostHttpSendRequestOk = AnonStructBe6bcbc15f8a1360; +pub type HostHttpSendRequestOkHeaders = AnonStruct77eaba63dfee299d; +pub type HostPathTypeOk = AnonStruct8dfa7f17f2083a52; +pub type HostSqliteBindArg1 = AnonStruct2782504baf739389; +pub type HostSqliteBindErr = AnonStruct22cf486058afc711; +pub type HostSqliteColumnValueErr = AnonStruct22cf486058afc711; +pub type HostSqliteColumnValueOk = BytesOrIntegerOrNullOrRealOrString; +pub type HostSqliteColumnValueOkPayload = BytesOrIntegerOrNullOrRealOrStringPayload; +pub type HostSqliteColumnValueOkTag = BytesOrIntegerOrNullOrRealOrStringTag; +pub type HostSqlitePrepareErr = AnonStruct22cf486058afc711; +pub type HostSqliteResetResult = HostSqliteBindResult; +pub type HostSqliteResetResultPayload = HostSqliteBindResultPayload; +pub type HostSqliteResetResultTag = HostSqliteBindResultTag; +pub type HostSqliteResetErr = AnonStruct22cf486058afc711; +pub type HostSqliteStepErr = AnonStruct22cf486058afc711; +pub type HostStderrWriteResult = HostStderrLineResult; +pub type HostStderrWriteResultPayload = HostStderrLineResultPayload; +pub type HostStderrWriteResultTag = HostStderrLineResultTag; +pub type HostStderrWriteBytesResult = HostStderrLineResult; +pub type HostStderrWriteBytesResultPayload = HostStderrLineResultPayload; +pub type HostStderrWriteBytesResultTag = HostStderrLineResultTag; +pub type HostStdinBytesErr = EndOfFileOrStdinErr; +pub type HostStdinBytesErrPayload = EndOfFileOrStdinErrPayload; +pub type HostStdinBytesErrTag = EndOfFileOrStdinErrTag; +pub type HostStdinLineErr = EndOfFileOrStdinErr; +pub type HostStdinLineErrPayload = EndOfFileOrStdinErrPayload; +pub type HostStdinLineErrTag = EndOfFileOrStdinErrTag; +pub type HostStdoutWriteResult = HostStdoutLineResult; +pub type HostStdoutWriteResultPayload = HostStdoutLineResultPayload; +pub type HostStdoutWriteResultTag = HostStdoutLineResultTag; +pub type HostStdoutWriteBytesResult = HostStdoutLineResult; +pub type HostStdoutWriteBytesResultPayload = HostStdoutLineResultPayload; +pub type HostStdoutWriteBytesResultTag = HostStdoutLineResultTag; +pub type HostTcpReadUntilResult = HostTcpReadExactlyResult; +pub type HostTcpReadUntilResultPayload = HostTcpReadExactlyResultPayload; +pub type HostTcpReadUntilResultTag = HostTcpReadExactlyResultTag; +pub type HostTcpReadUpToResult = HostTcpReadExactlyResult; +pub type HostTcpReadUpToResultPayload = HostTcpReadExactlyResultPayload; +pub type HostTcpReadUpToResultTag = HostTcpReadExactlyResultTag; +pub type MainForHostArg0 = OsStr; +pub type MainForHostArg0Payload = OsStrPayload; +pub type MainForHostArg0Tag = OsStrTag; + +// Generated Refcount Helpers + +impl HostCmdExecExitCodeResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostCmdExecExitCodeResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostCmdExecExitCodeResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostCmdExecExitCodeResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostCmdExecExitCodeResultTag::Ok => {}, + } + } +} + +impl HostIOErr { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostIOErrTag::AlreadyExists => {}, + HostIOErrTag::BrokenPipe => {}, + HostIOErrTag::Interrupted => {}, + HostIOErrTag::IsADirectory => {}, + HostIOErrTag::NotADirectory => {}, + HostIOErrTag::NotFound => {}, + HostIOErrTag::Other => { + let payload = value.payload_other(); + unsafe { payload.decref(roc_host); } + }, + HostIOErrTag::OutOfMemory => {}, + HostIOErrTag::PermissionDenied => {}, + HostIOErrTag::Unsupported => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostIOErrTag::AlreadyExists => {}, + HostIOErrTag::BrokenPipe => {}, + HostIOErrTag::Interrupted => {}, + HostIOErrTag::IsADirectory => {}, + HostIOErrTag::NotADirectory => {}, + HostIOErrTag::NotFound => {}, + HostIOErrTag::Other => { + let payload = value.payload_other(); + unsafe { payload.incref(amount); } + }, + HostIOErrTag::OutOfMemory => {}, + HostIOErrTag::PermissionDenied => {}, + HostIOErrTag::Unsupported => {}, + } + } +} + +impl AnonStruct32ddec9aa3de7110 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + { + let list = value.args; + if list.has_one_ref() { + for item_ref in list.allocation_items() { + let item = *item_ref; + unsafe { item.decref(roc_host); } + } + } + unsafe { list.decref(roc_host); } + } + { + let list = value.envs; + if list.has_one_ref() { + for item_ref in list.allocation_items() { + let item = *item_ref; + unsafe { item.decref(roc_host); } + } + } + unsafe { list.decref(roc_host); } + } + unsafe { value.program.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.args.incref(amount); } + unsafe { value.envs.incref(amount); } + unsafe { value.program.incref(amount); } + } +} + +impl UnixBytesOrUtf8OrWindowsU16s { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + UnixBytesOrUtf8OrWindowsU16sTag::UnixBytes => { + let payload = value.payload_unix_bytes(); + unsafe { payload.decref(roc_host); } + }, + UnixBytesOrUtf8OrWindowsU16sTag::Utf8 => { + let payload = value.payload_utf8(); + unsafe { payload.decref(roc_host); } + }, + UnixBytesOrUtf8OrWindowsU16sTag::WindowsU16s => { + let payload = value.payload_windows_u16s(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + UnixBytesOrUtf8OrWindowsU16sTag::UnixBytes => { + let payload = value.payload_unix_bytes(); + unsafe { payload.incref(amount); } + }, + UnixBytesOrUtf8OrWindowsU16sTag::Utf8 => { + let payload = value.payload_utf8(); + unsafe { payload.incref(amount); } + }, + UnixBytesOrUtf8OrWindowsU16sTag::WindowsU16s => { + let payload = value.payload_windows_u16s(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostCmdExecOutputResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostCmdExecOutputResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostCmdExecOutputResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostCmdExecOutputResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostCmdExecOutputResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl FailedToGetExitCodeOrNonZeroExitCode { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + FailedToGetExitCodeOrNonZeroExitCodeTag::FailedToGetExitCode => { + let payload = value.payload_failed_to_get_exit_code(); + unsafe { payload.decref(roc_host); } + }, + FailedToGetExitCodeOrNonZeroExitCodeTag::NonZeroExitCode => { + let payload = value.payload_non_zero_exit_code(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + FailedToGetExitCodeOrNonZeroExitCodeTag::FailedToGetExitCode => { + let payload = value.payload_failed_to_get_exit_code(); + unsafe { payload.incref(amount); } + }, + FailedToGetExitCodeOrNonZeroExitCodeTag::NonZeroExitCode => { + let payload = value.payload_non_zero_exit_code(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl IOErr { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + IOErrTag::AlreadyExists => {}, + IOErrTag::BrokenPipe => {}, + IOErrTag::Interrupted => {}, + IOErrTag::IsADirectory => {}, + IOErrTag::NotADirectory => {}, + IOErrTag::NotFound => {}, + IOErrTag::Other => { + let payload = value.payload_other(); + unsafe { payload.decref(roc_host); } + }, + IOErrTag::OutOfMemory => {}, + IOErrTag::PermissionDenied => {}, + IOErrTag::Unsupported => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + IOErrTag::AlreadyExists => {}, + IOErrTag::BrokenPipe => {}, + IOErrTag::Interrupted => {}, + IOErrTag::IsADirectory => {}, + IOErrTag::NotADirectory => {}, + IOErrTag::NotFound => {}, + IOErrTag::Other => { + let payload = value.payload_other(); + unsafe { payload.incref(amount); } + }, + IOErrTag::OutOfMemory => {}, + IOErrTag::PermissionDenied => {}, + IOErrTag::Unsupported => {}, + } + } +} + +impl AnonStruct3f89ee1e14924626 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { value.stderr_bytes.decref(roc_host); } + unsafe { value.stdout_bytes.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.stderr_bytes.incref(amount); } + unsafe { value.stdout_bytes.incref(amount); } + } +} + +impl AnonStruct3e7554e024207e25 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { value.stderr_bytes.decref(roc_host); } + unsafe { value.stdout_bytes.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.stderr_bytes.incref(amount); } + unsafe { value.stdout_bytes.incref(amount); } + } +} + +impl HostDirCreateResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostDirCreateResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostDirCreateResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostDirCreateResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostDirCreateResultTag::Ok => {}, + } + } +} + +impl HostDirListResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostDirListResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostDirListResultTag::Ok => { + let payload = value.payload_ok(); + { + let list = payload; + if list.has_one_ref() { + for item_ref in list.allocation_items() { + let item = *item_ref; + unsafe { item.decref(roc_host); } + } + } + unsafe { list.decref(roc_host); } + } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostDirListResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostDirListResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostEnvVarResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostEnvVarResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostEnvVarResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostEnvVarResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostEnvVarResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl EnvErrOrVarNotFound { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + EnvErrOrVarNotFoundTag::EnvErr => { + let payload = value.payload_env_err(); + unsafe { payload.decref(roc_host); } + }, + EnvErrOrVarNotFoundTag::VarNotFound => { + let payload = value.payload_var_not_found(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + EnvErrOrVarNotFoundTag::EnvErr => { + let payload = value.payload_env_err(); + unsafe { payload.incref(amount); } + }, + EnvErrOrVarNotFoundTag::VarNotFound => { + let payload = value.payload_var_not_found(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostEnvCwdResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostEnvCwdResultTag::Err => {}, + HostEnvCwdResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostEnvCwdResultTag::Err => {}, + HostEnvCwdResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostEnvExePathResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostEnvExePathResultTag::Err => {}, + HostEnvExePathResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostEnvExePathResultTag::Err => {}, + HostEnvExePathResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostFileReadBytesResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostFileReadBytesResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostFileReadBytesResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostFileReadBytesResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostFileReadBytesResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostFileDeleteResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostFileDeleteResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostFileDeleteResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostFileDeleteResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostFileDeleteResultTag::Ok => {}, + } + } +} + +impl HostFileReadUtf8Result { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostFileReadUtf8ResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostFileReadUtf8ResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostFileReadUtf8ResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostFileReadUtf8ResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostFileOpenReaderResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostFileOpenReaderResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostFileOpenReaderResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { decref_box_with(payload as RocBox, core::mem::align_of::(), false, None, roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostFileOpenReaderResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostFileOpenReaderResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { incref_box(payload as RocBox, amount); } + }, + } + } +} + +impl HostFileSizeInBytesResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostFileSizeInBytesResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostFileSizeInBytesResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostFileSizeInBytesResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostFileSizeInBytesResultTag::Ok => {}, + } + } +} + +impl HostFileIsExecutableResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostFileIsExecutableResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostFileIsExecutableResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostFileIsExecutableResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostFileIsExecutableResultTag::Ok => {}, + } + } +} + +impl HostFileTimeAccessedResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostFileTimeAccessedResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostFileTimeAccessedResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostFileTimeAccessedResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostFileTimeAccessedResultTag::Ok => {}, + } + } +} + +impl HostHttpSendRequestResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostHttpSendRequestResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostHttpSendRequestResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostHttpSendRequestResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostHttpSendRequestResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl BadBodyOrNetworkErrorOrOtherOrTimeout { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::BadBody => {}, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::NetworkError => {}, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::Other => { + let payload = value.payload_other(); + unsafe { payload.decref(roc_host); } + }, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::Timeout => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::BadBody => {}, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::NetworkError => {}, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::Other => { + let payload = value.payload_other(); + unsafe { payload.incref(amount); } + }, + BadBodyOrNetworkErrorOrOtherOrTimeoutTag::Timeout => {}, + } + } +} + +impl AnonStructBe6bcbc15f8a1360 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { 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; + unsafe { item.decref(roc_host); } + } + } + unsafe { list.decref(roc_host); } + } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.body.incref(amount); } + unsafe { value.headers.incref(amount); } + } +} + +impl AnonStruct77eaba63dfee299d { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { value._0.decref(roc_host); } + unsafe { value._1.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value._0.incref(amount); } + unsafe { value._1.incref(amount); } + } +} + +impl AnonStruct8bbc5017d7a8cb36 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { 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; + unsafe { item.decref(roc_host); } + } + } + unsafe { list.decref(roc_host); } + } + unsafe { value.method_ext.decref(roc_host); } + unsafe { value.uri.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.body.incref(amount); } + unsafe { value.headers.incref(amount); } + unsafe { value.method_ext.incref(amount); } + unsafe { value.uri.incref(amount); } + } +} + +impl HostLocaleGetResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostLocaleGetResultTag::Err => {}, + HostLocaleGetResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostLocaleGetResultTag::Err => {}, + HostLocaleGetResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostPathTypeResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostPathTypeResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostPathTypeResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostPathTypeResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostPathTypeResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl AnonStruct8dfa7f17f2083a52 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = value; + let _ = roc_host; + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = value; + let _ = amount; + } +} + +impl HostRandomSeedU64Result { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostRandomSeedU64ResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostRandomSeedU64ResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostRandomSeedU64ResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostRandomSeedU64ResultTag::Ok => {}, + } + } +} + +impl HostRandomSeedU32Result { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostRandomSeedU32ResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostRandomSeedU32ResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostRandomSeedU32ResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostRandomSeedU32ResultTag::Ok => {}, + } + } +} + +impl HostSqlitePrepareResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostSqlitePrepareResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostSqlitePrepareResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { decref_box_with(payload as RocBox, core::mem::align_of::(), false, None, roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostSqlitePrepareResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostSqlitePrepareResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { incref_box(payload as RocBox, amount); } + }, + } + } +} + +impl AnonStruct22cf486058afc711 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { value.message.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.message.incref(amount); } + } +} + +impl HostSqliteBindResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostSqliteBindResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostSqliteBindResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostSqliteBindResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostSqliteBindResultTag::Ok => {}, + } + } +} + +impl AnonStruct2782504baf739389 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { value.value.decref(roc_host); } + unsafe { value.name.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.value.incref(amount); } + unsafe { value.name.incref(amount); } + } +} + +impl BytesOrIntegerOrNullOrRealOrString { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + BytesOrIntegerOrNullOrRealOrStringTag::Bytes => { + let payload = value.payload_bytes(); + unsafe { payload.decref(roc_host); } + }, + BytesOrIntegerOrNullOrRealOrStringTag::Integer => {}, + BytesOrIntegerOrNullOrRealOrStringTag::Null => {}, + BytesOrIntegerOrNullOrRealOrStringTag::Real => {}, + BytesOrIntegerOrNullOrRealOrStringTag::String => { + let payload = value.payload_string(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + BytesOrIntegerOrNullOrRealOrStringTag::Bytes => { + let payload = value.payload_bytes(); + unsafe { payload.incref(amount); } + }, + BytesOrIntegerOrNullOrRealOrStringTag::Integer => {}, + BytesOrIntegerOrNullOrRealOrStringTag::Null => {}, + BytesOrIntegerOrNullOrRealOrStringTag::Real => {}, + BytesOrIntegerOrNullOrRealOrStringTag::String => { + let payload = value.payload_string(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostSqliteColumnValueResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostSqliteColumnValueResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostSqliteColumnValueResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostSqliteColumnValueResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostSqliteColumnValueResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostSqliteStepResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostSqliteStepResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostSqliteStepResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostSqliteStepResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostSqliteStepResultTag::Ok => {}, + } + } +} + +impl HostStderrLineResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostStderrLineResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostStderrLineResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostStderrLineResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostStderrLineResultTag::Ok => {}, + } + } +} + +impl HostStdinLineResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostStdinLineResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostStdinLineResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostStdinLineResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostStdinLineResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl EndOfFileOrStdinErr { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + EndOfFileOrStdinErrTag::EndOfFile => {}, + EndOfFileOrStdinErrTag::StdinErr => { + let payload = value.payload_stdin_err(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + EndOfFileOrStdinErrTag::EndOfFile => {}, + EndOfFileOrStdinErrTag::StdinErr => { + let payload = value.payload_stdin_err(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostStdinBytesResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostStdinBytesResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostStdinBytesResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostStdinBytesResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostStdinBytesResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostStdinReadToEndResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostStdinReadToEndResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostStdinReadToEndResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostStdinReadToEndResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostStdinReadToEndResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostStdoutLineResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostStdoutLineResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostStdoutLineResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostStdoutLineResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostStdoutLineResultTag::Ok => {}, + } + } +} + +impl HostTcpConnectResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostTcpConnectResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostTcpConnectResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { decref_box_with(payload as RocBox, core::mem::align_of::(), false, None, roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostTcpConnectResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostTcpConnectResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { incref_box(payload as RocBox, amount); } + }, + } + } +} + +impl HostTcpReadExactlyResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostTcpReadExactlyResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostTcpReadExactlyResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostTcpReadExactlyResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostTcpReadExactlyResultTag::Ok => { + let payload = value.payload_ok(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl HostTcpWriteResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostTcpWriteResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostTcpWriteResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostTcpWriteResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostTcpWriteResultTag::Ok => {}, + } + } +} + +impl HostUtcNowResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostUtcNowResultTag::Err => {}, + HostUtcNowResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostUtcNowResultTag::Err => {}, + HostUtcNowResultTag::Ok => {}, + } + } +} + +impl AnonStructBca0d23b5d625934 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { value.arch.decref(roc_host); } + unsafe { value.os.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.arch.incref(amount); } + unsafe { value.os.incref(amount); } + } +} + +impl AARCH64OrARMOrOTHEROrX64OrX86 { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + AARCH64OrARMOrOTHEROrX64OrX86Tag::AARCH64 => {}, + AARCH64OrARMOrOTHEROrX64OrX86Tag::ARM => {}, + AARCH64OrARMOrOTHEROrX64OrX86Tag::OTHER => { + let payload = value.payload_other(); + unsafe { payload.decref(roc_host); } + }, + AARCH64OrARMOrOTHEROrX64OrX86Tag::X64 => {}, + AARCH64OrARMOrOTHEROrX64OrX86Tag::X86 => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + AARCH64OrARMOrOTHEROrX64OrX86Tag::AARCH64 => {}, + AARCH64OrARMOrOTHEROrX64OrX86Tag::ARM => {}, + AARCH64OrARMOrOTHEROrX64OrX86Tag::OTHER => { + let payload = value.payload_other(); + unsafe { payload.incref(amount); } + }, + AARCH64OrARMOrOTHEROrX64OrX86Tag::X64 => {}, + AARCH64OrARMOrOTHEROrX64OrX86Tag::X86 => {}, + } + } +} + +impl LINUXOrMACOSOrOTHEROrWINDOWS { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + LINUXOrMACOSOrOTHEROrWINDOWSTag::LINUX => {}, + LINUXOrMACOSOrOTHEROrWINDOWSTag::MACOS => {}, + LINUXOrMACOSOrOTHEROrWINDOWSTag::OTHER => { + let payload = value.payload_other(); + unsafe { payload.decref(roc_host); } + }, + LINUXOrMACOSOrOTHEROrWINDOWSTag::WINDOWS => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + LINUXOrMACOSOrOTHEROrWINDOWSTag::LINUX => {}, + LINUXOrMACOSOrOTHEROrWINDOWSTag::MACOS => {}, + LINUXOrMACOSOrOTHEROrWINDOWSTag::OTHER => { + let payload = value.payload_other(); + unsafe { payload.incref(amount); } + }, + LINUXOrMACOSOrOTHEROrWINDOWSTag::WINDOWS => {}, + } + } +} + +impl AnonStruct69eee2ff6c448fed { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { value._0.decref(roc_host); } + unsafe { value._1.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value._0.incref(amount); } + unsafe { value._1.incref(amount); } + } +} + +impl HostEnvSetCwdResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + HostEnvSetCwdResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.decref(roc_host); } + }, + HostEnvSetCwdResultTag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostEnvSetCwdResultTag::Err => { + let payload = value.payload_err(); + unsafe { payload.incref(amount); } + }, + HostEnvSetCwdResultTag::Ok => {}, + } + } +} + +impl OsStr { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + OsStrTag::UnixBytes => { + let payload = value.payload_unix_bytes(); + unsafe { payload.decref(roc_host); } + }, + OsStrTag::Utf8 => { + let payload = value.payload_utf8(); + unsafe { payload.decref(roc_host); } + }, + OsStrTag::WindowsU16s => { + let payload = value.payload_windows_u16s(); + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + OsStrTag::UnixBytes => { + let payload = value.payload_unix_bytes(); + unsafe { payload.incref(amount); } + }, + OsStrTag::Utf8 => { + let payload = value.payload_utf8(); + unsafe { payload.incref(amount); } + }, + OsStrTag::WindowsU16s => { + let payload = value.payload_windows_u16s(); + unsafe { payload.incref(amount); } + }, + } + } +} + +impl TryType205 { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + let _ = roc_host; + match value.tag { + TryType205Tag::Err => {}, + TryType205Tag::Ok => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + TryType205Tag::Err => {}, + TryType205Tag::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([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] } => Try(I32, IOErr) + pub fn hosted_cmd_host_exec_exit_code(arg0: HostCmdExecExitCodeArgs) -> HostCmdExecExitCodeResult; + + /// Hosted symbol for Host.cmd_exec_output! + /// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] } => Try({ stderr_bytes : List(U8), stdout_bytes : List(U8) }, [FailedToGetExitCode(IOErr), NonZeroExitCode({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) })]) + pub fn hosted_cmd_host_exec_output(arg0: HostCmdExecOutputArgs) -> HostCmdExecOutputResult; + + /// Hosted symbol for Host.dir_create! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_create(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostDirCreateResult; + + /// Hosted symbol for Host.dir_create_all! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_create_all(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostDirCreateResult; + + /// Hosted symbol for Host.dir_delete_all! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_delete_all(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostDirCreateResult; + + /// Hosted symbol for Host.dir_delete_empty! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [DirErr(IOErr)]) + pub fn hosted_dir_delete_empty(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostDirCreateResult; + + /// Hosted symbol for Host.dir_list! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), [DirErr(IOErr)]) + pub fn hosted_dir_list(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostDirListResult; + + /// Hosted symbol for Host.env_cwd! + /// Roc signature: {} => Try([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [CwdUnavailable]) + pub fn hosted_env_cwd() -> HostEnvCwdResult; + + /// Hosted symbol for Host.env_dict! + /// Roc signature: {} => List(([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))])) + pub fn hosted_env_dict() -> RocList; + + /// Hosted symbol for Host.env_exe_path! + /// Roc signature: {} => Try([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [ExePathUnavailable]) + pub fn hosted_env_exe_path() -> HostEnvExePathResult; + + /// Hosted symbol for Host.env_platform! + /// Roc signature: {} => { arch : [AARCH64, ARM, OTHER(Str), X64, X86], os : [LINUX, MACOS, OTHER(Str), WINDOWS] } + pub fn hosted_env_platform() -> AnonStructBca0d23b5d625934; + + /// Hosted symbol for Host.env_set_cwd! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, IOErr) + pub fn hosted_env_set_cwd(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostEnvSetCwdResult; + + /// Hosted symbol for Host.env_temp_dir! + /// Roc signature: {} => [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] + pub fn hosted_env_temp_dir() -> UnixBytesOrUtf8OrWindowsU16s; + + /// Hosted symbol for Host.env_var! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [EnvErr(IOErr), VarNotFound([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))])]) + pub fn hosted_env_var(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostEnvVarResult; + + /// Hosted symbol for Host.file_delete! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_delete(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileDeleteResult; + + /// Hosted symbol for Host.file_hard_link! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_hard_link(arg0: UnixBytesOrUtf8OrWindowsU16s, arg1: UnixBytesOrUtf8OrWindowsU16s) -> HostFileDeleteResult; + + /// Hosted symbol for Host.file_is_executable! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_executable(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileIsExecutableResult; + + /// Hosted symbol for Host.file_is_readable! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_readable(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileIsExecutableResult; + + /// Hosted symbol for Host.file_is_writable! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(Bool, [FileErr(IOErr)]) + pub fn hosted_file_is_writable(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileIsExecutableResult; + + /// Hosted symbol for Host.file_open_reader! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], U64 => Try(Host.FileReader, [FileErr(IOErr)]) + pub fn hosted_file_open_reader(arg0: UnixBytesOrUtf8OrWindowsU16s, arg1: u64) -> HostFileOpenReaderResult; + + /// Hosted symbol for Host.file_read_bytes! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(List(U8), [FileErr(IOErr)]) + pub fn hosted_file_read_bytes(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileReadBytesResult; + + /// 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) -> HostFileReadBytesResult; + + /// Hosted symbol for Host.file_read_utf8! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(Str, [FileErr(IOErr)]) + pub fn hosted_file_read_utf8(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileReadUtf8Result; + + /// Hosted symbol for Host.file_rename! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_rename(arg0: UnixBytesOrUtf8OrWindowsU16s, arg1: UnixBytesOrUtf8OrWindowsU16s) -> HostFileDeleteResult; + + /// Hosted symbol for Host.file_size_in_bytes! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(U64, [FileErr(IOErr)]) + pub fn hosted_file_size_in_bytes(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileSizeInBytesResult; + + /// Hosted symbol for Host.file_time_accessed! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_accessed(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileTimeAccessedResult; + + /// Hosted symbol for Host.file_time_created! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_created(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileTimeAccessedResult; + + /// Hosted symbol for Host.file_time_modified! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try(U128, [FileErr(IOErr)]) + pub fn hosted_file_time_modified(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostFileTimeAccessedResult; + + /// Hosted symbol for Host.file_write_bytes! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], List(U8) => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_write_bytes(arg0: UnixBytesOrUtf8OrWindowsU16s, arg1: RocListWith) -> HostFileDeleteResult; + + /// Hosted symbol for Host.file_write_utf8! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], Str => Try({}, [FileErr(IOErr)]) + pub fn hosted_file_write_utf8(arg0: UnixBytesOrUtf8OrWindowsU16s, arg1: RocStr) -> HostFileDeleteResult; + + /// 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) -> HostHttpSendRequestResult; + + /// 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() -> HostLocaleGetResult; + + /// Hosted symbol for Host.path_type! + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({ is_dir : Bool, is_file : Bool, is_sym_link : Bool }, IOErr) + pub fn hosted_path_type(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostPathTypeResult; + + /// Hosted symbol for Host.random_seed_u32! + /// Roc signature: {} => Try(U32, [RandomErr(IOErr)]) + pub fn hosted_random_seed_u32() -> HostRandomSeedU32Result; + + /// Hosted symbol for Host.random_seed_u64! + /// Roc signature: {} => Try(U64, [RandomErr(IOErr)]) + pub fn hosted_random_seed_u64() -> HostRandomSeedU64Result; + + /// 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) -> HostSqliteBindResult; + + /// 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) -> HostSqliteColumnValueResult; + + /// 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: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], Str => Try(Host.SqliteStmt, { code : I64, message : Str }) + pub fn hosted_sqlite_prepare(arg0: UnixBytesOrUtf8OrWindowsU16s, arg1: RocStr) -> HostSqlitePrepareResult; + + /// Hosted symbol for Host.sqlite_reset! + /// Roc signature: Host.SqliteStmt => Try({}, { code : I64, message : Str }) + pub fn hosted_sqlite_reset(arg0: *mut u64) -> HostSqliteBindResult; + + /// Hosted symbol for Host.sqlite_step! + /// Roc signature: Host.SqliteStmt => Try(Bool, { code : I64, message : Str }) + pub fn hosted_sqlite_step(arg0: *mut u64) -> HostSqliteStepResult; + + /// Hosted symbol for Host.stderr_line! + /// Roc signature: Str => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_line(arg0: RocStr) -> HostStderrLineResult; + + /// Hosted symbol for Host.stderr_write! + /// Roc signature: Str => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_write(arg0: RocStr) -> HostStderrLineResult; + + /// Hosted symbol for Host.stderr_write_bytes! + /// Roc signature: List(U8) => Try({}, [StderrErr(IOErr)]) + pub fn hosted_stderr_write_bytes(arg0: RocListWith) -> HostStderrLineResult; + + /// Hosted symbol for Host.stdin_bytes! + /// Roc signature: {} => Try(List(U8), [EndOfFile, StdinErr(IOErr)]) + pub fn hosted_stdin_bytes() -> HostStdinBytesResult; + + /// Hosted symbol for Host.stdin_line! + /// Roc signature: {} => Try(Str, [EndOfFile, StdinErr(IOErr)]) + pub fn hosted_stdin_line() -> HostStdinLineResult; + + /// Hosted symbol for Host.stdin_read_to_end! + /// Roc signature: {} => Try(List(U8), [StdinErr(IOErr)]) + pub fn hosted_stdin_read_to_end() -> HostStdinReadToEndResult; + + /// Hosted symbol for Host.stdout_line! + /// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_line(arg0: RocStr) -> HostStdoutLineResult; + + /// Hosted symbol for Host.stdout_write! + /// Roc signature: Str => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_write(arg0: RocStr) -> HostStdoutLineResult; + + /// Hosted symbol for Host.stdout_write_bytes! + /// Roc signature: List(U8) => Try({}, [StdoutErr(IOErr)]) + pub fn hosted_stdout_write_bytes(arg0: RocListWith) -> HostStdoutLineResult; + + /// Hosted symbol for Host.tcp_connect! + /// Roc signature: Str, U16 => Try(Host.TcpStream, Str) + pub fn hosted_tcp_connect(arg0: RocStr, arg1: u16) -> HostTcpConnectResult; + + /// 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) -> HostTcpReadExactlyResult; + + /// 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) -> HostTcpReadExactlyResult; + + /// 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) -> HostTcpReadExactlyResult; + + /// Hosted symbol for Host.tcp_write! + /// Roc signature: Host.TcpStream, List(U8) => Try({}, Str) + pub fn hosted_tcp_write(arg0: *mut u64, arg1: RocListWith) -> HostTcpWriteResult; + + /// 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: {} => Try(U128, [ClockBeforeEpoch]) + pub fn hosted_utc_now() -> HostUtcNowResult; + +} + +/// 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. +#[cfg(not(no_roc_std_helpers))] +pub struct DefaultAllocators; + +#[cfg(not(no_roc_std_helpers))] +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 = checked_add_usize(length, size_storage_bytes, "roc_alloc allocation size overflow"); + + 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 = checked_add_usize(new_length, size_storage_bytes, "roc_realloc allocation size overflow"); + 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. +#[cfg(not(no_roc_std_helpers))] +pub struct DefaultHandlers; + +#[cfg(not(no_roc_std_helpers))] +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. +#[cfg(not(no_roc_std_helpers))] +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/src/sqlite.rs b/src/sqlite.rs new file mode 100644 index 00000000..c8177735 --- /dev/null +++ b/src/sqlite.rs @@ -0,0 +1,632 @@ +use core::mem::ManuallyDrop; +use std::cell::RefCell; +use std::ffi::{c_char, c_int, c_void, CStr, CString}; + +use crate::roc_platform_abi::*; +use crate::{roc_host, roc_u8_list_from_slice}; + +// The generated glue represents the `Host.SqliteStmt` backing `Sqlite.Stmt` 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 = HostSqliteBindArg1; +type NativePath = UnixBytesOrUtf8OrWindowsU16s; +type NativePathTag = UnixBytesOrUtf8OrWindowsU16sTag; + +const SQLITE_STMT_BOX_ALIGN: usize = core::mem::align_of::(); + +struct SqliteStatement { + connection: *mut libsqlite3_sys::sqlite3, + stmt: *mut libsqlite3_sys::sqlite3_stmt, +} + +#[derive(Clone, PartialEq, Eq)] +enum SqlitePath { + Unix(Vec), + Windows(Vec), +} + +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_path_from_native( + path: NativePath, + roc_host: &RocHost, +) -> Result { + match path.tag { + NativePathTag::UnixBytes => unsafe { + let bytes = ManuallyDrop::into_inner(path.payload.unix_bytes); + let path = SqlitePath::Unix(bytes.as_slice().to_vec()); + bytes.decref(roc_host); + Ok(path) + }, + NativePathTag::Utf8 => unsafe { + let text = ManuallyDrop::into_inner(path.payload.utf8); + #[cfg(unix)] + let path = SqlitePath::Unix(text.as_str().as_bytes().to_vec()); + #[cfg(windows)] + let path = SqlitePath::Windows(text.as_str().encode_utf16().collect()); + #[cfg(not(any(unix, windows)))] + let path = { + text.decref(roc_host); + return Err(( + libsqlite3_sys::SQLITE_CANTOPEN, + "UTF-8 database paths are not supported on this host".to_string(), + )); + }; + text.decref(roc_host); + Ok(path) + }, + NativePathTag::WindowsU16s => unsafe { + let u16s = ManuallyDrop::into_inner(path.payload.windows_u16s); + let path = SqlitePath::Windows(u16s.as_slice().to_vec()); + u16s.decref(roc_host); + Ok(path) + }, + } +} + +#[cfg(windows)] +extern "C" { + fn sqlite3_open16(filename: *const c_void, pp_db: *mut *mut libsqlite3_sys::sqlite3) -> c_int; +} + +fn sqlite_open_native(path: &SqlitePath) -> Result<*mut libsqlite3_sys::sqlite3, (c_int, String)> { + let mut connection: *mut libsqlite3_sys::sqlite3 = core::ptr::null_mut(); + + match path { + SqlitePath::Unix(bytes) => { + #[cfg(unix)] + { + let cpath = CString::new(bytes.as_slice()).map_err(|_| { + ( + libsqlite3_sys::SQLITE_ERROR, + "database path contained an interior nul byte".to_string(), + ) + })?; + 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)); + } + + Ok(connection) + } + + #[cfg(not(unix))] + { + let _ = bytes; + Err(( + libsqlite3_sys::SQLITE_CANTOPEN, + "Unix database paths are not supported on this host".to_string(), + )) + } + } + SqlitePath::Windows(u16s) => { + #[cfg(windows)] + { + if u16s.iter().any(|unit| *unit == 0) { + return Err(( + libsqlite3_sys::SQLITE_ERROR, + "database path contained an interior nul code unit".to_string(), + )); + } + + let mut nul_terminated = u16s.clone(); + nul_terminated.push(0); + let err = unsafe { + sqlite3_open16(nul_terminated.as_ptr() as *const c_void, &mut connection) + }; + if err != libsqlite3_sys::SQLITE_OK { + let message = sqlite_errmsg(connection, err); + return Err((err, message)); + } + + Ok(connection) + } + + #[cfg(not(windows))] + { + let _ = u16s; + Err(( + libsqlite3_sys::SQLITE_CANTOPEN, + "Windows database paths are not supported on this host".to_string(), + )) + } + } + } +} + +fn sqlite_get_connection( + path: SqlitePath, +) -> Result<*mut libsqlite3_sys::sqlite3, (c_int, String)> { + SQLITE_CONNECTIONS.with(|cell| { + for (conn_path, connection) in cell.borrow().iter() { + if conn_path == &path { + return Ok(*connection); + } + } + + let connection = sqlite_open_native(&path)?; + cell.borrow_mut().push((path, 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: [] }, + 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: NativePath, + query: RocStr, +) -> HostSqlitePrepareResult { + let roc_host = roc_host(); + let path = match sqlite_path_from_native(path, roc_host) { + Ok(path) => path, + Err((code, message)) => { + unsafe { query.decref(roc_host) }; + return try_sqlite_prepare_err(sqlite_error(code, &message, roc_host)); + } + }; + let query_string = query.as_str().to_owned(); + unsafe { query.decref(roc_host) }; + + let connection = match sqlite_get_connection(path) { + 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() { + unsafe { binding.decref(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 +} diff --git a/src/tcp.rs b/src/tcp.rs new file mode 100644 index 00000000..4049b08b --- /dev/null +++ b/src/tcp.rs @@ -0,0 +1,264 @@ +use core::mem::ManuallyDrop; +use std::ffi::c_void; +use std::io::{self, BufRead, BufReader, Read, Write}; +use std::net::TcpStream; + +use crate::roc_platform_abi::*; +use crate::{roc_host, roc_u8_list_from_slice}; + +// The `Host.TcpStream` backing `Tcp.Stream` 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: [] }, + 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 +} 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 deleted file mode 100644 index 323b70c4..00000000 --- a/tests/cmd-test.roc +++ /dev/null @@ -1,119 +0,0 @@ -app [main!] { pf: platform "../platform/main.roc" } - -import pf.Stdout -import pf.Cmd -import pf.Arg exposing [Arg] - -# Tests all error cases in Cmd functions. - -main! : List Arg => Result {} _ -main! = |_args| - - # exec! - expect_err( - Cmd.exec!("blablaXYZ", []), - "(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}))" - )? - - # exec_cmd! - expect_err( - Cmd.new("blablaXYZ") - |> Cmd.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}))" - )? - - # exec_output! - expect_err( - Cmd.new("blablaXYZ") - |> Cmd.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: \"\"}))" - )? - - # Test StdoutContainsInvalidUtf8 - using printf to output invalid UTF-8 bytes - 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})}))" - )? - - # exec_output_bytes! - expect_err( - Cmd.new("blablaXYZ") - |> Cmd.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: []}))" - )? - - # exec_exit_code! - expect_err( - Cmd.new("blablaXYZ") - |> Cmd.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 = - Cmd.new("cat") - |> Cmd.arg("non_existent.txt") - |> Cmd.exec_exit_code!()? - - if exit_code == 1 then - Ok({})? - else - Err(FailedExpectation( - """ - - - Expected: - 1 - - - Got: - ${Inspect.to_str(exit_code)} - - """ - ))? - - Stdout.line!("All tests passed.")? - - Ok({}) - -expect_err = |err, expected_str| - if Inspect.to_str(err) == expected_str then - Ok({}) - else - Err(FailedExpectation( - """ - - - Expected: - ${expected_str} - - - Got: - ${Inspect.to_str(err)} - - """ - )) \ No newline at end of file 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